Last active
July 14, 2020 06:41
-
-
Save zaceno/239e384dd914f1cb83a4d4b36af25ea2 to your computer and use it in GitHub Desktop.
progressive possible future hyperapp app example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
In the following examples I will show how an app could be written in | |
progressively improved potential hyperapp versions | |
It may not always look like an improvement. I'd have to use an *actually* | |
complicated app as an example to really prove the benefit. So just use | |
your imagination :) | |
But first: the basic app I will be successively improving on. This should | |
work in hyperapp as it is today. | |
*/ | |
app({ | |
state: { | |
counter: 0, | |
username: 'Barney Fooley', | |
fortune: { | |
current: 0, | |
possibles: [ | |
'You will win the lottery!', | |
'You will meet a new friend today!', | |
'Today you will learn a valuable lesson!' | |
] | |
}, | |
}, | |
actions: { | |
nextFortune (state) { | |
state.fortune.current = (state.fortune.current + 1 ) % state.fortune.possibles.length | |
state.counter = state.counter + 1 | |
return state | |
} | |
}, | |
view (state, actions) { | |
return h('div', [ | |
h('p', ['Hello ', state.username]), | |
h('p', ['Your fortune is:']), | |
h('div', {class: 'fortune-box'}, [state.fortune.possibles[state.fortune.current]]), | |
h('button', {onclick: actions.nextFortune}, ['New fortune']), | |
h('p', ['Youve asked for ', state.counter, ' fortunes']), | |
]) | |
} | |
}) | |
/* | |
We'd like to bind actions to specific parts of state. | |
We already have a mechanism for doing this and it's called `app({})`. | |
So the basic idea is to let each app({state: ..., actions: {...} }) | |
bind the actions to operate on that specific state. | |
State and action objects we pass to other actions (and the view) are | |
broken apart into separate trees with the same structure | |
For the actions in nested apps, we only provide the sub-trees of the state | |
and action trees | |
in this example, the top level state object would be: | |
{ | |
nbrOfSwitchets: 5, | |
username: 'Barney Fooley' | |
fortune: { | |
currentIndex: 0, | |
possibles: [ | |
'You will win the lottery!', | |
'You will meet a new friend today!', | |
'Today you will learn a valuable lesson!' | |
] | |
} | |
} | |
the actions object at the top level would be: | |
{ | |
nextFortune, | |
fortune: { | |
next | |
} | |
} | |
Note that this approach does away with the need for plugins. You can export | |
them as apps from their respective modules, and attach them to the state tree | |
with whatever naming you see fit. | |
*/ | |
app({ | |
state: { | |
counter: 0, | |
username: app({ | |
state: 'Barney Fooley', | |
}), | |
fortune: app({ | |
state: { | |
current: 0, | |
possibles: [ | |
'You will win the lottery!', | |
'You will meet a new friend today!', | |
'Today you will learn a valuable lesson!' | |
] | |
}, | |
actions: { | |
next: (state) => ({current: (state.current + 1) % state.possibles.length }) | |
}, | |
}), | |
}, | |
actions: { | |
nextFortune (state, actions) { | |
actions.fortune.next() | |
return {counter: state.counter + 1} | |
} | |
}, | |
view (state, actions) { | |
return h('div', [ | |
h('p', ['Hello ', state.username]), | |
h('p', ['Your fortune is:']), | |
h('div', {class: 'fortune-box'}, [state.fortune.possibles[state.fortune.current]]), | |
h('button', {onclick: actions.nextFortune}, ['New fortune']), | |
h('p', ['Youve asked for ', state.counter, ' fortunes']), | |
]) | |
} | |
}) | |
/* | |
I know they're not popular, but I'm really tempted to add getters in | |
the mix here to clean up some logic in the view: | |
*/ | |
app({ | |
state: { | |
counter: 0, | |
username: app({ | |
state: 'Barney Fooley', | |
}), | |
fortune: app({ | |
state: { | |
current: 0, | |
possibles: [ | |
'You will win the lottery!', | |
'You will meet a new friend today!', | |
'Today you will learn a valuable lesson!' | |
] | |
}, | |
getters: { | |
currentFortune: state => state.possibles[state.current] | |
}, | |
actions: { | |
next: (state) => ({current: (state.current + 1) % state.possibles.length }) | |
}, | |
}), | |
}, | |
actions: { | |
nextFortune (state, actions) { | |
actions.fortune.next() | |
return {counter: state.counter + 1} | |
} | |
}, | |
view (state, actions, getters) { | |
return h('div', [ | |
h('p', ['Hello ', state.username]), | |
h('p', ['Your fortune is:']), | |
h('div', {class: 'fortune-box'}, [getters.fortune.currentFortune()]), | |
h('button', {onclick: actions.nextFortune}, ['New fortune']), | |
h('p', ['Youve asked for ', state.counter, ' fortunes']), | |
]) | |
} | |
}) | |
/* | |
Now there's still the asymmetry of only the top-level app being allowed to have a view | |
I propose that we let sub-apps define views as well, and allow them to be called in | |
upper level views. This is like @autoSponge 's solution earlier, but doesn't require | |
us to keep track of sub-app's dom nodes in the state tree. | |
...and it makes getters less of an issue, so let's get rid of them... | |
*/ | |
app({ | |
state: { | |
counter: 0, | |
username: app({ | |
state: 'Barney Fooley', | |
view: state => h('p', ['Hello', state]) | |
}), | |
fortune: app({ | |
state: { | |
current: 0, | |
possibles: [ | |
'You will win the lottery!', | |
'You will meet a new friend today!', | |
'Today you will learn a valuable lesson!' | |
] | |
}, | |
actions: { | |
next: (state) => ({current: (state.current + 1) % state.possibles.length }) | |
}, | |
view: (state, actions) => h('div', {class: 'fortune-box'}, [state.possibles[state.current]]) | |
}), | |
}, | |
actions: { | |
nextFortune (state, actions) { | |
actions.fortune.next() | |
return {counter: state.counter + 1} | |
} | |
}, | |
view (state, actions, views) { | |
return h('div', [ | |
views.username(), | |
h('p', ['Your fortune is:']), | |
views.fortune(), | |
h('button', {onclick: actions.nextFortune}, ['New fortune']), | |
h('p', ['Youve asked for ', state.counter, ' fortunes']), | |
]) | |
} | |
}) | |
/* | |
Having come this far with actions and views bound grouped with their own states, I see | |
little reason to keep them organized in the master-state structure at all. | |
It would be nice to just be able to define our apps separately, and render them | |
in the tree wherever we want. | |
For the implementation we either need to make sure the "top" level | |
app always rerenders when any of the children change (= dependency tracking), | |
or we need to keep track of where each app is attached in the dom (using lifecycle methods), | |
so that they can update just their own sub-trees when they change. | |
Also notice that this requires each app instance to expose it's actions, it's state (or getters), | |
and a "render" method, for other actions to use. We're going into OOP territory which may not sit | |
well with some. | |
*/ | |
const username = app({ | |
state: 'Barney Foley', | |
view: name => h('p', ['Hello', name]) | |
}) | |
const fortune = app({ | |
state: { | |
current: 0, | |
possibles: [ | |
'You will win the lottery!', | |
'You will meet a new friend today!', | |
'Today you will learn a valuable lesson!' | |
] | |
}, | |
actions: { | |
next: ({current, possibles}) => ({current: (current + 1) % possibles.length}) | |
}, | |
view: ({current, possibles}) => h('div', {class: 'fortune-box'}, [possibles[current]]) | |
}) | |
const main = app({ | |
state: 0, | |
actions: { | |
nextFortune: counter => { | |
fortune.actions.next() | |
return counter + 1 | |
} | |
}, | |
view: ({counter}) => h('div', [ | |
username.render(), | |
h('p', ['Your fortune is:']), | |
fortune.render(), | |
h('button', {onclick: getNextFortune}, ['New fortune']), | |
h('p', ['Youve asked for ', counter, ' fortunes']), | |
]) | |
}) | |
main.render() | |
/* | |
The above example is nice and clean with some loose coupling, but I'm not into the OOP-ness of | |
it, and how each app has to expose a bunch of accessors. We started with apps just doing partial application | |
of state to actions and views, and I'd like to get back to that: | |
*/ | |
const usernameView = app('Barney Fooley').view(name => h('p', ['Hello', name])) | |
const fortune = (_ => { | |
const a = app({ | |
current: 0, | |
possibles: [ | |
'You will win the lottery!', | |
'You will meet a new friend today!', | |
'Today you will learn a valuable lesson!' | |
] | |
}) | |
return { | |
next: a.action(({current, possibles}) => ({current: (current + 1) % possibles.length})), | |
view: a.view(({current, possibles}) => h('div', {class: 'fortune-box'}, [possibles[current]])) | |
} | |
})() | |
const mainView = (_ => { | |
const a = app(0) | |
const nextFortune = a.action(counter => { | |
fortune.next() | |
return counter + 1 | |
}) | |
return a.view(counter => h('div', [ | |
usernameView(), | |
h('p', ['Your fortune is:']), | |
fortune.view(), | |
h('button', {onclick: nextFortune}, ['New fortune']), | |
h('p', ['Youve asked for ', counter, ' fortunes']), | |
])) | |
})() | |
mainView() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment