Skip to content

Instantly share code, notes, and snippets.

@zaceno
Last active July 14, 2020 06:41
Show Gist options
  • Save zaceno/239e384dd914f1cb83a4d4b36af25ea2 to your computer and use it in GitHub Desktop.
Save zaceno/239e384dd914f1cb83a4d4b36af25ea2 to your computer and use it in GitHub Desktop.
progressive possible future hyperapp app example
/*
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