Why mixin needs to specify its namespace in state and actions? The main purpose of mixin with state or actions will be almost always adding these two props under key with name of mixin (because naturally we want to avoid namespace pollution) so what if we scope state and actions in mixin automatically?
Now it's working like this: counter | app.
I propose that we change a little bit behavior of mixins. It would give more sense if we automatically scope mixin's state and actions so mixin doesn't need to use own name inside of itself. Change will be needed in the core of HyperApp where we must store state and actions of every mixin under mixin name. And mixin's actions must be initialized with scoped state and actions.
But if we need global addition of mixin's state/actions to app (to first level without storing under mixin name) then we can inform app about globals for example by nesting under global prop in mixin (example) which will return mixin structure under "mixin.global.state" or "mixin.global.actions" and in HyperApp will be verification if mixin contains "global" and if so then it will store it immediately to first level of state or actions.
After this change would be possible to write mixins like this: counter | app.
_You can switch between "master" (actual behavior) and "scope" (with proposal) branch for comparison._
For this purpose it would be better to change mixins prop from array to object (key = mixin name) because then we can use mixin's name as a namespace for storing mixin's state (state.mixinName) and actions (actions.mixinName).
And another advantage would be optional change of mixin's name so for example if you need use "router" as a key in your business logic and at the same time you need package Router then you can include this package under different name in mixins (e.g. mixins: { _router }).
mixins to object.state and actions in mixin.P.S.: I'm working on this experiment so later I can send working example.
The main purpose of mixin with state or actions will be almost always adding these props under a key with name ... because naturally we want to avoid namespace pollution
I think this is true. It is true in the case of the Router and it should be true in most if not all cases because we want to avoid namespace pollution / collisions as you are saying.
While I basically agree with this, there needs to be an "opt out". Or perhaps an "opt in" to this namespacing behavior on a per mixin basis. As long as that's there I'm 100% on board
@zaceno So recommend users to namespace their mixins, but don't enforce it?
Yes, sort of. I mean I see the value of also being able to add mixins that are not restricted to a particular part of the state. Things that add general behavior, or behavior that crosses over / combines the power of other mixins.
Example: a logger
Or perhaps collecting all your validation logic in a validation mixin (that's not clearly a great example, but I think it should be an option)
@zaceno Scoped mixins would make @MatejMazur's pseudo-stateful components (components that can manage their own state sort-of) easier to implement.
It works like this.
const Component = mixin => (props, children) =>
props.view ? mixin : mixin.view(props.state, props.actions)
Then create your component like so.
const Counter = Component({
state: {
counter: {
value: 0
}
},
actions: {
counter: {
up: state => ({
counter: {
value: state.counter.value + 1
}
})
}
},
view: (state, { up }) =>
<div>
<h1>{state.value}</h1>
<button onclick={up}>+</button>
</div>
})
Having said that, scoped mixins also pose a difficult implementation problem, specially scoped actions. Not impossible. I had a working example, but I don't like how it makes actions seem less transparent.
For example, the router currently updates the state like this:
match:(state, actions, data) => ({ router: match(data) })
With scoped mixins, I would expect actions in my mixin to let me update the state in this way instead:
match:(state, actions, data) => match(data)
/*
match returns โ {
match: match,
params: params
}
but the action would be wrapped in a way so the above becomes:
{
router: {
match: match,
params: params
}
}
*/
Yeah I think it's probably difficult to nail down the perfect pattern that would fit everything.
Even so I'm not down on this -- it's absolutely something I would use! -- just not every time.
I think the best plan is to play with some patterns for this (outside of hyperapp's core should be possible) until we find the one that works the best. I'm off to play with an Idea I've got right now...
@jbucaran You're probably right with your implementation worry.
So we can just enable this possibility in the core of HyperApp and then I can create or extend package for scoped mixins.
Maybe I can just change my mixin package to default behavior as scoped mixin. Because router and other mixins don't need this package and they don't have view part so they are probably better without scoping.
Then I only want mixins prop as object and some kind of notification which app recognizes and uses for scoping. What do you think?
For example when app call this mixin:
const SomeMixin = mixin({
state: 0,
actions: { increase: () => "foo" }
})
like this mixins[i](app) then this mixin returns object:
{
scope: {
state: 0,
actions: { increase: () => "foo" }
}
}
and app knows that it must store this under name of mixin.
After that it would be better to rename my mixin() to something like scope().
@jbucaran If you agree, can you help me with that, please? :pray:
@MatejMazur This actually works now.
component.jsconst component = mixin => (props, children) =>
props.view ? mixin : mixin.view(props.state, props.actions)
counter.jsconst set = value => ({ counter: { value } })
const Counter = component({
state: set(0),
actions: {
counter: {
up: ({ counter }) => set(counter.value + 1),
down: ({ counter }) => set(counter.value - 1)
}
},
view: ({ value }, { up, down }) =>
<div>
<h1>{value}</h1>
<button onclick={up}>+</button>
<button onclick={down}>-</button>
</div>
})
index.jsapp({
view: (state, actions) =>
<div>
<Counter state={state.counter} actions={actions.counter} />
</div>,
mixins: [Counter]
})
@MatejMazur One way to improve this is by not passing the entire state into actions, but a fragment of the state scoped to its current namespace.
See here. So, instead of state โ state[namespaceKey].
@jbucaran Yep, that's possible but I want to avoid namespace clutter what you have in example (you have counter in actions and if you want use action in action then you must write actions.counter.someAction which is the point of this issue that's why I want real scoping I want actions.someAction). And your solution with partial state is what we did before but I especially need help with mixins prop as object because there will begin some fun. ๐
One of goals is that name of mixin can be changed from outside (you can change name of mixin in mixins prop). This scoped mixin doesn't use its name inside of itself.
@MatejMazur I think the only drawback with the example above is how you define your actions. It would be simpler if you received the counter state instead of the global state.
But with a little change to how we initialize actions, it's possible, so your actions would be implemented like so.
actions: {
counter: {
up: ({ value }) => set(value + 1),
down: ({ value }) => set(value - 1)
}
},
What do you think? ๐
Now, I just need to make sure this doesn't have any unpleasant side effects. ๐ค
@jbucaran I think that you go harder way. You can just ask if returned mixin has key scope - see comment and if so then you just initialize actions like this:
var result = {
[mixin.name]: action(
state[mixin.name],
actions[mixin.name],
emit(...).data,
emit
)
}
..
and result can be merged with app state.
@MatejMazur The implementation is not important. What's important is to clarify what behavior we want.
@jbucaran And what behavior do you want? I don't see any problem with this. It just allows scoped mixins. That's it. Or maybe I don't understand something..
Scoped mixin could be written like this:
mixin/scope({
state: 0 // => stored under state.mixinName: 0
actions: {
increase: ... // stored under actions.mixinName.increase: ...
}
..
})
The only unsolved thing what comes to my mind is what to do with nested mixins? Because we should be able to prevent name collisions. So maybe we need store this nested mixins under concatenated name which will be concatenation of parent-mixin name and name of mixin. For example:
const Counter = mixin/scope({
mixins: { Foo } // => stored under state.counterFoo or state["counter/foo"] or state["counter_foo"], ...
})
@MatejMazur The conversation is about how you implement the counter, not how we need to change app.js. That part we can decide later.
See: https://github.com/hyperapp/hyperapp/issues/269#issuecomment-312517057
Here is what we are trying to achieve in easy to understand pseudo-code.
This is the app.
```js
app({
state: {
color: "Red"
},
actions: {
switch: state => ({ color: "Blue" })
},
mixins: { A }
})
Let's define mixin A.
```js
const A = () => ({
state: {
value: 1
},
actions: {
up: state => ({ value: state.value + 1 })
},
mixins: { B }
})
And mixin B.
```js
const B = () => ({
state: {
flag: true
},
actions: {
toggle: state => ({ flag: !state.flag })
}
})
As you can see mixins A and B state and actions seem to be isolated from the global state/actions. In fact, the state either mixin action receives is a *fragment* of the global state.
This, of course, _is not_ how mixins work at the moment. The proposal _is to make_ them work that way.
So, for completion, here is how the global state and actions are updated behind the scenes during init.
For convenience, let's call the global state/actions objects STATE and ACTIONS.
```js
var STATE = {
name: "Red",
A: {
value: 1,
B: {
flag: true
}
}
}
var ACTIONS = {
rename: Function,
A: {
up: Function,
B: {
toggle: Function
}
}
}
Another way to think about this is namespaces.
But this is much more than just a namespace. The real difficulty is to correctly call those actions passing the piece of the global state that corresponds to that specific namespace/scope.
Not every app uses a mixins, so all of this stuff is only relevant if you _are_ using mixins.
I still need to fully grasp all the implications, but basically this allows you to create mixins that don't need to know how to grab their state/actions from the global state.
It's also limiting because then there is no way for a mixin to merge something in the top level (probably not a good idea anyway).
This also makes actions extremely convenient to work with in deeply scoped mixins.
Notice how mixins can be nested.
My worry is that this looks a lot like stateful components.
But it isn't. This is definitely a single state tree.
/cc @zaceno @MatejMazur
The best consensus about the @MatejMazur and I wants :heart:
This is what I had in mind since the begining of the Components discussion.
Im frustrated it is still called Mixins but Im glad to be able to use this in my app.
STATE need a little more work about isolation, but this can wait a PR after it will be fully implemented at this stage.
@jbucaran It looks that you are on the same page. ๐
With this approach would be possible to keep merging in the top level (this would be default behavior of mixins) and merging under name of mixin (scoped mixin). But there was question about events in scoped mixins -> they should be scoped too because otherwise it breaks whole idea. Scoped mixins can work independently as app() so there should not be any clutter with namespace.
I'm closing this issue because @zaceno and @jbucaran helped me with better implementation which doesn't need intervention in the core of HyperApp. ๐
Here is experimental package which allows to write mixins in scope: link.
@MatejMazur This looks fantastic! I think we might even want to support this package officially.
@zaceno?
@MatejMazur From just reading the code, it looks to me like it does exactly what I'd expect it to do. I'd have to play with it a little to be sure (codepen?) -- but still: great work!
Update: I can't tell if actions returning null/undefined, or promises still work as they should. Also: is deep-scoping (several levels of namespacing) supported? Doesn't have to be -- just curious
@jbucaran Officially supported how? As a @hyperapp/scoped-mixin package? Totally!
@zaceno Here is repo with example of usage: https://github.com/MatejMazur/hyperapp-component-experiment/tree/master/src
src/counter.js uses this package.
@MatejMazur
Ok looked at it now, and think we can do even better -- ~I think we can make it so you don't even have to pass the state and actions to your Coutner component~. Looking into it...
Edit: Brainfart on my part. No way around passing state and actions to component-mixins using this pattern. That needs to be handled in core.
@zaceno is deep-scoping (several levels of namespacing) supported?
If you mean composing mixins, then nope, don't think so. I think this is desirable given that mixins can compose in hyperapp (thanks to @jamen).
I'm just dropping another idea to tackle this: why not providing a local state/action for mixins?
Something like React component states. I already feel tomatoes thrown at me ๐
mixin({
state: 0 // stored in the global state
localState: 1 // stored locally (not available in the global state)
actions: {
increase: ... // stored in the global actions
},
localActions: {
decrease: ... // stored locally (not available in the global actions)
}
..
})
Naming is ugly
@ngryman that's an interesting pattern -- I think most naturally implemented outside of core hyperapp. I think that should be possible.
@ngryman I'm just dropping another idea to tackle this: why not providing a local state/action for mixins?
@MatejMazur figured out how to create _fragments_ without intervention from core, so I am positive the pattern you have proposed can be achieved in a similar manner.
I already feel tomatoes thrown at me ๐
๐๐
Ok I was talking a little with @jamen about this and I came up with a nasty idea.
What about exposing a hook that let us customize how mixins are merged by the core.
const SuperDeathScoper = () => ({
events: {
mergeMixins: (mixins) => (
// ... do not flatten but keep nested
return mixins
)
}
})
app({
...,
mixins: [A, B, SuperDeathScoper]
})
๐บ or ๐ ?
@ngryman Correct me if I am wrong, but since mixins are no longer allowed to expose their own mixins, this is not needed? ๐ค
Hyperapp 0.15.0 solves this in the core with modules. ๐
Most helpful comment
Hyperapp 0.15.0 solves this in the core with modules. ๐