I've got an app that will have multiple image galleries in one view, and they're instances of the same component.
What's the most functional / hyperapp way of storing separate data (currently selected item, etc...) per instance?
Hm good question. What ways have you considered?
You say "multiple galleries" - is it a fixed number (could you for instance, name them like "topGallery" and "bottomGallery" or is it dynamic, where there could be zero or five depending on how many the user has? I think your answer may depend on that.
I suppose I prefer the idea of there being a dynamic number and have them register themselves in the state. That way the concept can be used for any widget, no matter the context it's being used in.
For this project it's a fixed number, but I'm still not sure exactly where to name them (would you pass it a prop and then use that id to interact with the state?
To be honest I'm pretty new to hyperapp so I'm not really sure where to start...
sounds like a perfect use case for state slice
@Pyrolistical how do the state slices know the difference between instances?
I get that they are good for isolating concerns, but I'm still not sure how to store different state in a slice per instance of a corresponding component...
Would you mind showing me a simple example? (sorry if I'm asking obvious questions!)
@snalld the way I would approach it is to create a Galleries module that would take care of managing a collection of Gallery types, I would start with something like:
export default {
state: {
collection: {
one: { photos: ['family.png'] }
two: { photos: ['holiday.png'] }
}
},
actions: {
addGallery: (state, actions, gallery) => ({
collection: { ...state.collection, {
[gallery.id]: gallery
}}
}),
removeGallery: (state, actions, id) => ({
collection: Object.keys(state.collection)
.filter(key => key !== id)
.map(key => state.collection[key])
}),
addPhotoToGallery: (state, actions, { id, src }) => ({
collection: { ...state.collection, {
[id]: { photos: state.galleries[id].photos.concat(src) }
}}
})
}
}
Then you could have a view that renders galleries:
const Gallery = gallery =>
section([
h1(gallery.id),
gallery.photos.map(src => img({ src }))
])
export const Galleries = (state, actions) =>
ul(
Object.keys(state.collection)
.map(key => state.collection[key])
.map(Gallery),
button({ onclick: e => actions.addGallery({
id: state.collection.length
photos: ['default.jpg']
}), 'Create Gallery')
)
Then you can use that in your app as a module/component
import galleries, { Galleries } from './galleries'
app({
modules: { galleries },
view: (state, actions) => Galleries(state.galleries, actions.galleries)
})
Excuse the contrived example, hope that helps!
Thanks @lukejacksonn that's super helpful!
I'll let you know how I go
No worries! I find myself using this kind of _container_ pattern with firebase database refs where collections are usually objects (rather than arrays). It seems weird at first (and makes you long for Object.keys) because they appear hard to manage in an immutable fashion but so long as you don't nest too much, there is some sense in it.. I think 馃槄
Yeah I've used firebase before so I'm not unaccustomed to that way of structuring data. Using a more nested structure for this project though, but I'm not too worried about tweaking what you've got going on to suit my data
Thanks again for the quick replies!
One interesting option might be to create a module for a single gallery, and transform it into a multi-gallery module using a generic instance-to-collection function. (A "higher order module" if you will).
One reason for this would be to avoid having to repeat the logic for handling multiple instances of a thing, if you do that a lot in your app.
You'd also have to write some utility functions to access the specific instance state and actions.
Come to think of it, that might be a reason to keep "getters" functionality in core. (They're on their way out along with thunks)
Hey @zaceno that sounds more like what i'm after
Would you mind writing a really quick example of how I might go about that?
Sorry @snalld - it was just an idea off the top of my head. I gave it a quick shot but it turned out a little trickier than I first thought.
It's an interesting problem though, so I'll keep thinking about it and report back if I figure it out :)
@zaceno @snalld that sounds like what I am trying to do with #431, you can also see my comment here: https://github.com/hyperapp/hyperapp/issues/432#issuecomment-339442386
Here is the implementation of initActions() that I use in my project (in typescript though):
interface InitActionsPayload<
S extends Hyperapp.State,
A extends Hyperapp.Actions<S>
> {
state: S
// actions to apply on a part of the state
actions: Hyperapp.InternalActions<S, A>
// dot separated string
path: string
// update function provided by Hyperapp
update: Hyperapp.Update<any>
}
function initActions<S extends Hyperapp.State, A extends Hyperapp.Actions<S>>(
payload: InitActionsPayload<any, any>
): A {
const { state, actions, path, update } = payload
const result = {}
const actionsState = get(state, path)
const actionUpdate = result => {
if (typeof result === "function") {
// get(object, "prop1.prop2.prop3") is equivalent to object.prop1.prop2.prop3
return actionUpdate(result(get(state, path)))
} else {
// set(object, "prop1.prop2.prop3", value) is the immutable version of object.prop1.prop2.prop3 = value
// it returns a copy of the state, updated with the given value.
return get(update(set(state, path, result)), path)
}
}
Object.keys(actions).forEach(key => {
result[key] = function(data) {
let actionResult = actions[key](actionsState, result, data)
return typeof actionResult === "function"
? actionResult(actionUpdate)
: update(merge(state, path, actionResult))
}
})
return result as A
}
And here is an example usage:
content(state, _, { path }: ContentPayload) {
// here, path is a dot separated string
return update => {
return initActions({
state,
path,
actions: contentActions,
update
})
}
},
Hope this helps.
@Mytrill thanks for sharing that example. Unfortunately I wasn't sure how to interpret it.
Anyhow @snalld I took a shot at it, and it seems to work out ok. It has some flaws (like instantiable modules can't have submodules or nested actions, and actions can't use thunks), but it should be a good start:
@snalld Do you think we can close here or is this still ongoing? 馃憢
/cc @zaceno @Mytrill @lukejacksonn
Hopefully our comments put @snalld on the right track. Given that this was more of a general advice question than an issue I鈥檓 happy to close. Feel free to join #help on the slack channel for more assistance 馃
I'm happy for you to close it
@snalld sorry I took the liberty 馃槄馃檱