A difficult test of UI design patterns turns out to be a simple task:
The design philosophy of Thermite is still a bit out of reach for me, but I think I understand how lenses and prisms can be used to combine Spec
s, but not how to invoke a parent's action.
This question was written for version
0.10.5
, which may change by the reader's time.
The application being built will be a simple counter, where the increment button increases the count value, and the decrement button decreases it. We will do this by making a generic button
component, then using multiple of them in the counter
component. The design is as follows:
counter:
- CounterState = { count :: Int
, incButton :: ButtonState
, decButton :: ButtonState
}
- init = { count: 0
, incButton: initButton
, decButton: initButton
}
- CounterAction = Increment
| Decrement
| IncButton (ButtonAction CounterAction)
| DecButton (ButtonAction CounterAction)
button:
- ButtonState = Unit
- initButton = unit
- ButtonAction parentAction = Clicked parentAction
In the button's ButtonAction
, I'm stating that I need a parentAction
to perform on Clicked
. I've kept this as a type parameter to maintain a generic interface. However, this means we have to supply one from somewhere, so I've allowed a parameter in the button's Spec:
buttonSpec :: forall eff props parentAction
. { _onClick :: parentAction }
-> Spec eff ButtonState props (ButtonAction parentAction)
buttonSpec parentActions = T,simpleSpec performAction renderButton
This means I will use the _onClick
action supplied here when I dispatch
in my render:
renderButton :: Render ButtonState props (ButtonAction parentAction)
renderButton dispatch _ state _ =
[ -- ... html stuff
, button [onClick $ dispatch $ Clicked parentActions._onClick]
[] -- with fill text etc
]
Now, the tricky part is integrating two buttonSpec
s in a single counterSpec
. For this, we leverage two lenses incButton
and decButton
, and two prisms _IncButton
and _DecButton
, which do the obvious state and action related tasks:
counterSpec :: forall eff props
. Spec eff CounterState props CounterAction
counterSpec =
T.simpleSpec performAction render
where
incButton = focus incButton _IncButton
$ buttonSpec Increment
decButton = focus decButton _DecButton
$ buttonSpec Decrement
which we would use in the counter's performAction
and render
functions, using the lens and prism Thermite provides baked in:
render :: Render CounterState props CounterAction
render dispatch props state children =
[ text $ "Count: " <> state.count
] <> (incButton ^. _render) dispatch props state children
<> (decButton ^. _render) dispatch props state children
performAction :: PerformAction eff CounterState props CounterAction
performAction Increment _ _ =
modifyState $ count %~ (\x -> x + 1)
performAction Decrement _ _ =
modifyState $ count %~ (\x -> x - 1)
performAction action@(IncButton _) props state =
(incButton ^. _performAction) action props state
performAction action@(DecButton _) props state =
(decButton ^. _performAction) action props state
This should be pretty straight forward. When we actually want to Increment
or Decrement
, we modify the parent's state. Otherwise, we look into the subcomponent-specific actions, but only just enough to tell who it should belong to! When it belongs to either the increment or decrement buttons, we pass the data to it.
That's the design for my ideal scenario - delegate the decision for "details" later, through polymorphism, and let composition handle plumbing. However, when testing this, react does not seem to dispatch the child component's parental actions. I'm not sure if this is how dispatch
is intended to be designed, or what the issue actually is, but I have a git repo with a working minimal example of the error.
I've created a pull request on your GitHub repo. I think you're overcomplicating things by trying to pass the action to the Button. What's more in line with the Thermite way of doing things is to let the Button emit his action and then use a Prism in the parent component to map the Button's action into the parent's action space.
So instead of having 4 actions on the parent you just have 2:
data ParentAction = Increment | Decrement
You then handle these the usual way by incrementing or decrementing a counter in your state. Now in order to trigger these ParentAction
s with your buttons you emit a simple Clicked
Action from your buttons and use prisms to map these to Increment or Decrement:
_IncButton :: Prism' CounterAction ButtonAction
_IncButton = prism' (const Increment) $ case _ of
Increment -> Just Clicked
_ -> Nothing
_DecButton :: Prism' CounterAction ButtonAction
_DecButton = prism' (const Decrement) $ case _ of
Decrement -> Just Clicked
_ -> Nothing
Now all that is left to use these Prism's to focus on the right Click actions:
inc = T.focus incButton _IncButton $ buttonSpec {_value: "Increment"}
dec = T.focus decButton _DecButton $ buttonSpec {_value: "Decrement"}
And vóila!