Combining Components
The ability to combine components into a full application is an important part of the Plait framework. Each component exposes its functionality only as init
, update
, and view
functions. This restricted interface makes it impossible to know the implementation details of a component, which is a good foundation for creating truly modular code.
Using the Counter
component as an example, we can see how small components can be easily composed and reused throughout an application.
export function init () {
return {
count: 0
}
}
export function update (state, action) {
switch (action.type) {
case 'DECREMENT':
return state.update('count', x => x - 1)
case 'INCREMENT':
return state.update('count', x => x + 1)
}
}
export function view (state, dispatch) {
return (
<div>
<button ev-click={dispatch({ type: 'DECREMENT' })}>-</button>
<span>{state.get('count')}</span>
<button ev-click={dispatch({ type: 'INCREMENT' })}>+</button>
</div>
)
}
As a slightly contrived example, we can build a component which renders two counters. The state of this counter will have two properties, one for each of the counters.
import * as Counter from 'Counter'
export function init () {
return {
counter1: Counter.init(),
counter2: Counter.init()
}
}
Notice how this component doesn't need to know what the state of a Counter
is. It only needs to call Counter.init
to get the initial state for each counter.
Next the component needs to handle actions to update each of the counters.
export function update (state, action) {
switch (action.type) {
case 'COUNTER1':
return state.update('counter1', counterState => Counter.update(counterState, action.$fwdAction))
case 'COUNTER2':
return state.update('counter2', counterState => Counter.update(counterState, action.$fwdAction))
}
}
Since this component doesn't have any behaviour of its own, all it needs to do is delegate any actions to Counter.update
and use the result to update the counter's state. The interesting part about this is action.$fwdAction
. This is a special property which we will look at more in a minute.
Finally, this component needs a view.
import Plait from 'plait'
const fwd = Plait.forwardDispatch
export function view (state, dispatch) {
const c1State = state.get('counter1')
const c2State = state.get('counter2')
const c1Dispatch = fwd({ type: 'COUNTER1' }, dispatch, c1State)
const c2Dispatch = fwd({ type: 'COUNTER2' }, dispatch, c2State)
return (
<div>
{Counter.view(c1State, c1Dispatch)}
{Counter.view(c2State, c2Dispatch)}
</div>
)
}
Similar to the update
function, this component only needs to pass each counter's state to Counter.view
and let the Counter
component worry about doing the rendering.
In order to send actions from the Counter
component to this component's update
function, each counter needs a forwarder. This can be created with Plait.forwardDispatch
. These forwarders will intercept any action from the Counter
component and instead dispatch a new action directly to this component. The original action is stored in a special property, $fwdAction
, so that it can be used later.