purescript-thermite is a PureScript wrapper for purescript-react. It does not provide all of the functionality of React, but instead provides a clean API to the most commonly-used parts of its API. It is possible to use purescript-react for more specialized use cases.
bower update
pulp build
pulp test -r cat > html/index.js
You can also now use npm test to run the test command above.
Thermite components are defined in parts:
- A type of actions, which represents the actions a user can take on our component
- A type of states, which represents the internal state of our component
- An initial state
- A rendering function, which takes the current component state and properties, and creates a HTML document
- A function which interprets actions, by modifying the state and/or running some (possibly asynchronous) computations
Here is an example. We'll build a component which displays the value of a integer-valued counter.
First of all, we need to import some modules:
import Thermite as T
import React as R
import React.DOM as R
import React.DOM.Props as RP
import ReactDOM as RDOMIn our component, users will be able to take two actions - increment and decrement - which will be represented as buttons later:
data Action = Increment | DecrementThe state of our component is just an integer:
type State = { counter :: Int }The initial state is zero:
initialState :: State
initialState = { counter: 0 }Our rendering function uses the React.DOM.* modules to create a HTML document containing a label and two buttons. The buttons' onclick handlers are given functions which generate the correct actions. The dispatch function, which is passed as the first argument to render, can be used to build such a function, by providing an action:
render :: T.Render State _ Action
render dispatch _ state _ =
[ R.p' [ R.text "Value: "
, R.text $ show state.counter
]
, R.p' [ R.button [ RP.onClick \_ -> dispatch Increment ]
[ R.text "Increment" ]
, R.button [ RP.onClick \_ -> dispatch Decrement ]
[ R.text "Decrement" ]
]
]The performAction function interprets actions by passing a function to the state update function, which is responsible for updating the state using record updates:
performAction :: T.PerformAction State _ Action
performAction Increment _ _ = void (T.cotransform (\state -> state { counter = state.counter + 1 }))
performAction Decrement _ _ = void (T.cotransform (\state -> state { counter = state.counter - 1 }))Note: PerformAction returns a coroutine, which can emit many asynchronous state updates using cotransform. This approach also allows us to create asynchronous and/or chunked action handlers (using AJAX or websockets, for example):
getIncrementValueFromServer :: Aff Int
performAction :: T.PerformAction State _ Action
performAction Increment _ _ = do
Just amount <- lift getIncrementValueFromServer
void $ T.cotransform $ \state -> state { counter = state.counter + amount }With these pieces, we can create a Spec for our component:
spec :: T.Spec State (T.WithChildren ()) Action
spec = T.Spec {performAction, render}Note that the new purescript-react needs some typechecking assistance for
props-Specneeds an extra{ children :: Children | props }field in its props, yet that field is not necessary when creating a react element with itspropsargument.
WithChildren propsis just an alias for{ children :: Children | props }.
Finally, in main, the defaultMain function from the
purescript-thermite-dom
library can be used to render our component to the document body by specifying the initial state:
import Thermite.DOM (defaultMain)
main = defaultMain spec (const initialState) "MyComponent" {}The Spec type is an instance of the Semigroup and Monoid type classes. These instances can be used to combine different components with the same state and action types.
In practice, the state and action types will not always match for the different subcomponents, so Thermite provides combinators for changing these type arguments: focus and foreach. These combinators are heavily inspired by the OpticUI library.
See the example project for examples of these kinds of composition.
focus (and the related functions focusState and match) are used to enlarge the state and action types, to make it possible to embed a component inside a larger component.
focus takes a lens, which identifies the state type as a part of the state type of the larger component, and a prism, which identifies all actions of the smaller component as actions for the larger component. focusState is used when only the state type needs to be changed, and match is used when only the action type needs to be changed.
As a simple example, we can combine two subcomponents by using a Tuple to store both states, and Either to combine both sets of actions:
spec1 :: Spec S1 _ A1
spec2 :: Spec S2 _ A2
spec :: Spec (Tuple S1 S2) _ (Either A1 A2)
spec = focus _1 _Left spec1 <> focus _2 _Right spec2Here, _1 and _Left embed spec1 inside spec, using the left components of both the state Tuple and the Either type of actions. _2 and _Right similarly embed spec2, using the right components.
focus is responsible for directing the various actions to the correct components, and updating the correct parts of the state.
split is used to handle child components which might not be present, for
example, when a parent object contains a Maybe state.
type Parent = { child :: Maybe child }
_child :: LensP Parent (Maybe Child)
_child = lens _.child (_ { child = _ })
_ChildAction :: PrismP ParentAction ChildAction
childSpec :: Spec Child _ ChildAction
spec :: Spec Parent _ ParentAction
spec = focus _child _ChildAction $ split _Just childSpecWhere focus embeds a single subcomponent inside another component, foreach embeds a whole collection of subcomponents.
foreach turns a Spec eff state props action into a Spec eff (List state) props (Tuple Int action). Note that the state type has been wrapped using List, since the component now tracks state for each element of the collection. Also, the action type has been replaced with Tuple Int action. This means that when an action occurs, it is accompanied by the index of the element in the collection which it originated from.