Skip to content

Instantly share code, notes, and snippets.

@zaceno
Last active August 25, 2017 07:47
Show Gist options
  • Save zaceno/54297f9c3a3d2443bfb3678427b7b37a to your computer and use it in GitHub Desktop.
Save zaceno/54297f9c3a3d2443bfb3678427b7b37a to your computer and use it in GitHub Desktop.
Stateful Hyperapp Components
/*
This function lets you define components you can use in your views, which are
stateful, i e, they embed their own hyperapp app.
The idea is to let you build apps according to what I, for lack of better terms,
call the "Vue architecture" -- where your app is built up of a tree of stateful
components. Parent components communicate with children by the props passed in
the view. Children may communicate back to parents via events the parents listen
to.
Example demo: https://codepen.io/zaceno/pen/KveOeQ?editors=1010
Fair warning: It is not yet clear to me, that there is any real benefit to
the "Vue architecture" over the Elm-architecture. If there are any cases where
this approach is advisable, it may be when creating small, reusable ui elements
which need to manage state related to interactions (like for instance, a radio
knob, a vertical slider, a datepicker, et c)
... I was just curious if this was possible, without too much trouble.
Apparently it is :)
## mini usage docs:
### Nesting Componets
1) Define a component, by passing a app as first argument into `nestable`,
and as the second argument a function. The function is similar to how mixins
are defined in hyperapp, in that it recieves an emit. Unlike mixins, however,
you can use all the regular app options (mixins, view, et c)
const Counter = nestable(app, emit => ({
state: {...},
actions: {...},
events: {...},
mixins: [...],
view: (state, actions) => ...
}))
2) In the view of the main app, or a parent component, render the previously
defined component:
app({
...
view: (state, actions) => <main>
<Counter key="mycounter" value={state.counterValue} />
</main>
})
Note: it is important that stateful components are keyed (unique among siblings)
### Parent-child communication ###
Parents communicate with children via setting the component props. Every time
the child is rendered (including the first time), the child component app will
recieve the `parent:update` with the props passed to the child on that render.
The child should implement some sort of handling of these props in that event.
### Child-parent communication ###
Props passed to the child beginning with `on` are special. If, in the parent
view, there is a function bound to `onfoo`,
view: (state, actions) => <main>
<Counter key="counter" onchange={val => actions.counterChanged(val)} />
</main>
...then this function will be called when the child emits
`emit('self:foo', data)`.
counter Counter = subapp(emit => ({
actions: {
increment: (state, actions) => update => {
update({value: state.value + 1})
emit('self:changed', state.value)
}
}
}))
The data emitted is passed as first and only argument to the handler
**Danger!** When syncing copies of a value between parent and child, be *very careful*
you don't accidentally create an infinte loop! It's a good idea to use
hyperapp's `update` event to cancel updare & render if the state hasn't
changed.
*/
const nestable = (app, appDefFn) => props => ({
tag: 'embedded-',
children: [],
data: {
key: props.key || 'defaultkey',
onupdate: el => { el.emit('parent:update', props) },
oncreate: el => {
const events = {}
for (let n in props) {
if (n.substr(0,2) === 'on') {
events['self:' + n.substr(2)] = (s, a, data) => props[n](data)
}
}
var emit
const appOpts = appDefFn((msg, data) => (emit && emit(msg, data)))
appOpts.events = Object.assign(appOpts.events, events)
appOpts.root = el
emit = app(appOpts)
el.emit = emit
el.emit('parent:update', props)
}
}
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment