Repository contains an proposed implementation of the mnt function for the hyperapp V2 library. It will enable to decouple actions and implement fully reusable components.
It is assumed that yo ualready familiar with Hyperapp V2 concepts, if not then read them first.
There is only one single mnt function that binds together property setter and getter and optional parent mapper.
This function returns a mapper that can be applied to state or action. This mapper should be pased via props to a component's "view" function. View function should apply a mapper to actions.
Example:
/////////////// Counter component ///////////////
const IncrementAction = counter => counter + 1
const DecrementAction = counter => counter - 1
const ResetAction = counter => 0
const Counter = ({ counter, map }, children) =>
h('div', {}, [
h('h3', {}, counter),
h('button', { onclick: map(IncrementAction) }, '+'),
h('button', { onclick: map(DecrementAction) }, '-'),
h('button', { onclick: map(ResetAction) }, '0'),
...children
])
...
/////////////// Main View ///////////////
const initialState = {
counter1: 10,
counter2: 21
}
...
const view = state =>
h('div', {}, [
h(Counter,
{
counter: state.counter1,
map: mnt(s => s.counter1, (s, v) => s.counter1 = v)
}
),
h(Counter,
{
counter: state.counter2,
map: mnt(s => s.counter2, (s, v) => s.counter2 = v)
}
)
])
...Here is the CodePen.
View doesn't know anything (and should not) about app state. The only thing view has is a props.
If component need to modify the app state then view should use action.
View should map actions from global state to component state.
We can achieve this by 2 steps:
- Each view should have
mapfunction that is injected via props - View should assign to
vnodea mapped action rather then action directly. I.e. instead of:onclick: Incrementit should beonclick: map(Increment).
The proposed mnt function is used to construct a such map function.
If component also embeds other components that follows above paradigm then parent component should pass own props.map as a third parameter to mnt function
Below is a small example of how components can be composed to each other.
Counter component
Edit component
CounterWithTitle component constructed on top of previous two
/// //////////// Counter component ///////////////
const IncrementAction = counter => counter + 1
const DecrementAction = counter => counter - 1
const ResetAction = counter => 0
const Counter = ({ counter, map }, children) =>
h('div', {}, [
h('h3', {}, counter),
h('button', { onclick: map(IncrementAction) }, '+'),
h('button', { onclick: map(DecrementAction) }, '-'),
h('button', { onclick: map(ResetAction) }, '0'),
...children
])
/// //////////// Custom edit box. Can edit any string value ///////////////
// model: string
const SetTextAction = (state, text) => text
const Edit = ({ value, map }) =>
h('input', {
type: 'text',
oninput: [
map(SetTextAction),
e => e.target.value
],
value: value
})
// Counter with editable title
const ToggleEditing = state => ({ ...state, editing: !state.editing })
const CounterWithTitle = ({ title, counter, editing, map }) =>
h('div', {}, [
!editing && h('p', {}, title),
editing && h(Edit, { value: title, map: mnt(s => s.title, (s, v) => s.title = v, map) }),
h(Counter, { counter: counter, map: mnt(s => s.counter, (s, v) => s.counter = v, map) }),
h('button', { onclick: map(ToggleEditing) }, 'Toggle title edit')
])
// Main view
const initialState = {
counter: 0,
counterWithTitle: {
title: 'Hello Hyperapp!',
counter: 21
}
}
const CounterWithTitleGetter = s => s.counterWithTitle
const mainView = state =>
h('div', {}, [
h(Counter,
{
counter: state.counter,
map: mnt(s => s.counter, (s, v) => s.counter = v)
}
),
h(CounterWithTitle,
{
...CounterWithTitleGetter(state),
map: mnt(CounterWithTitleGetter, (s, v) => s.counterWithTitle = v)
}
)
])
app(
{
init: initialState,
node: document.getElementById('app'),
view: mainView
}
)mnt function can be described using TypeScript like below.
type Action<S> = (state: S, options?: any) => S
type Mapper<X,Y> = ( (s: X) => Y )
& ( (s: X, v: Y) => X )
& ( (action: Action<X>) => Action<Y> );
type Getter<S,U> = (s: S) => U
type Setter<S,U> = (s: S, v: U) => S
type Mount<S,U,G> = ( (get: Getter<S,U>, set: Setter<S,U>, parent: Mapper<G,S>) => Mapper<G,U> )
| ( (get: Getter<G,U>, set: Setter<G,U> ) => Mapper<G,U> )
// S - is type of current state
// U - is type of state that we want to extract from current and inject to children
// G - type of application global state
// if p is not specified then it is assumed that S is G
declare function mnt<S,U,G> (get: Getter<S,U>, set: Setter<S,U>, parent: Mapper<G,S>): Mapper<G,U>;
declare function mnt<U,G> (get: Getter<G,U>, set: Setter<G,U>): Mapper<G,U>;