Closed jorgebucaran closed 7 years ago
@jbucaran Yeah, I know that but this is not problem. 😄 Problem was with components
everywhere (as prop in app, as argument in view, without destructuring you need write longer version of arrow function with return statement (assigning components to variables) or you don't use JSX for components, and it's not usable for more than 5 or 10 components). And even if you nest components it still doesn't look cleaner. (I tried that with my first example in Slack and you were right that it's full of new things)
I hoped that someone knows how to achieve that with HyperApp because I didn't know how but now I know that it's not possible with current API.. 😝 so you can move on how you want! 👍 I don't have any other ideas.
@jbucaran Sure I'll put a PR together. And perahaps a demo of components using other components. And mixins with components (a k a Widgets :) ) It's good to have concrete examples to discuss :)
Single state tree, single actions tree and single component tree. With a system of namespaces, namespaces for component name and namespaces for isolation ID like it does in Cyclejs.
Everything live into the same objects at start, but is pretty isolated. When the view function of component is called, we give to it a substet of the tree according to its isolation.
Like a Fractal. Because my english is poor :
So, we can create component into seperate file, but when a component as added to an app, it will be merged in isolation.
This will allow to use componenet as app too :
app(Counter(props));
And according to use case, the actions dec: (state, actions) => state
will also receive as input a subset of the state and action according to the Component which is the caller.
@Swizz Yes! That's exactly what I want to achieve (your graph)! That's how I did it but I don't know how to store and calls instances by id if it's allowed to have multiple apps because I don't know how to say to component which app called it..
I think this is not too difficult. When a component is registered/instantiated, we can give it a random ID, if one is not given (like key for vnode).
I did something similar with mixins and ES6 features https://github.com/hyperapp/hyperapp/issues/238#issuecomment-310999839
@Swizz Yep, but again, you just need another prop in app. So we're back at the beginning. I achieved this without another prop and arg but I don't know how to keep communication between the right app and component - so you need id for app but then you need to tell to component which app calls it.
@Swizz @MatejMazur just don't forget about the need to access the rest of the state and actions. A counter is a very ~unusual~ untypical example, in that it's own state/actions are enough for it.
I'd be (honestly!) interested in seeing an implementation of an example where the components as well as the main app need to access eachother's state and actions.
Component are stateless, so, we do not have component which are part of two apps. Here is the same "Model" but here is two different component.
As the Component and the App live with the same State and Actions tree, this is easy to handle
parent->child
comunication, the Parent could call every action of its childs.
And for child->parent
, this is as easy as adding a .parent
into the partial component State and Actions tree.
@zaceno This isn't difficult because you can set which part of state would be accesible. So we can create for example something like:
component(state, actions, props, children) => {...
// call app state
state.global.something
// call component state
state.something
}
You can just set state before calling component. (this is easy doable)
@Swizz But do you think that it's not possible without some dependency prop (depends: ...
) like you have in comment, don't you? Because it increases boilerplate..
@Swizz & @MatejMazur Yeh, it's possible sure. Still I feel like in a more typical example with lots of cross-component-state-sharing, it wont look as clean and nice as the counter examples. In fact I suspect that not using fragmented state will look cleaner in those more typical cases. But I'd love to be proven wrong!
We need a more realistic app-example to demonstrate our proposals on.
@zaceno It it's possible than it will be much more cleaner because you don't need to know full path to instance's state, actions etc. You just simply build component like this.
We can rearrange the API to be more user friendly. And in fact, accessing parent state/actions is a terrible pattern.
In most case, this is more comfortable to use properties for parent->child
communication and events for child->parent
comunication
import { h, app } from "hyperapp"
import Counter from "./counter"
app({
state: {
title: "Distance",
unit: "km",
},
view: (state, actions, components, emit) =>
<main>
<h1>{state.title}</h1>
{components.Counters.map((Component) =>
<Component/>
)}
</main>,
components: state => ({
Counters: [
<Counter initialValue={5}, unit={state.unit}, onchange={value => emit('changed', value)}/>
]
}),
events: {
changed: (state, actions, value) => { alert(value) }
}
})
@MatejMazur Yeah I know but: a) Your example is still using counters, which do not demonstrate typical complexity. b) You can use assocPath to bind/scope actions & state to specific state-branches. Why bake it into a component solution and make it non-optional?
@Swizz Agree with parent state/actions. I just want to argue that it's possible. 😉
@MatejMazur We can add a parent propertie to each substract of state and actions tree for those who like to deal with terrible patterns.
@zaceno Do you have a more complex example in mind ?
@zaceno You can use assocPath and that's how I do but it can be done automatically. If you build app you don't want work with something what is not related to business logic. And there is still possibility to use assocPath but you still need to know so many things about parent and that's antipattern. Component based approach is about ignorance of parents. 😄 For example counter doesn't need to know how many instances has or where is used it wants to know only own state, actions and inserted props and children. That's all.
@Swizz My solution was based on outer event mediator which sends instance's state, actions from app to component there it connects these things together then it returns new instance's state to app for storing. But this mediator didn't know id of app so if you want multiple apps then I don't know how to achieve that.
@Swizz We can easily create example by using any existing project and our goal would be just insert this project as component to another project (e.g. insert tic tac toe to website built with HyperApp). The purpose of component is simple. You just change/add a few things and it can work like component in another app. In my example you just add name
prop and change app()
to component()
(changes in tic tac toe) and that's it.
@MatejMazur It's no secret that I like the component based approach. I like not having to know about stuff 😜 ,
Still don't think hyperapp should to do anything special with states/actions. In my world-view, the counter-examples could be their own apps.
I like @Swizz idea of passing the emit function to views. @jbucaran suggested that too at some point, but can't remember the context.
@Swizz More typical example imo: Anything with forms and validation. There could be multiple types of inputs (sliders, calendar widgets, autocompleting text inputs) and they would each have validation rules based on what was in the other fields. (Like if you selected a weekend in the calendar app, you could only select a certain range on a time slider, stuff like that)
And they would all have to disable while the form was being submitted. Some fields would require a backend server call for their validation.
@zaceno everything related to communication between components is based on storing props in their parent so this is not purpose of component but rather philosophy behind one way data binding.
:) I think I should just shut up and let @MatejMazur @Swizz @naugtur and @andyrj settle this...
(Because you all seem to be somewhat on the same page, and I don't want to come in and mess that up)
Imagine reverse React+Redux approach. Stateful components in React are large apps in HyperApp (multiple apps) and Redux/Elm approach is similar to behavior in these apps. App knows everything inside even local states of components (keep in mind that these states are stored in app.state). So you use mutiple apps only for really large and totally independent parts and components will be used for building these apps.
@zaceno In your example, in my mind, Disabling an input is the job of the parent not the input itself.
I am comming from Vue an Cyclejs, and from both, Component isolation is a panacea. Component is not allowed to think outer their own business.
You are attempting to give to more power to component. That why we are alot against Hyperapp capabilities. Because we are not aligned with Elm principle.
A component is a function. And an app is a component which is the root of the component tree.
Stateful components in React are large apps in HyperApp (multiple apps)
That why, I am moving from the Hyperapp router to a global one to settle an Hyper'app per route.
@Swizz
And an app is a component which is the root of the component tree.
All I'm saying is: if you want to structure apps that way with hyperapp, then an app is a tree of apps. If you also want a common state-store for all your components/apps, like Vuex, then that's another app (your store app). That's fine with me.
Doesn't mean we need to change anything about how app()
works (maybe there are some minor things, but not sure what yet)
@Swizz could you join to Slack channel? We can try to build something. 😄
@zaceno
Here, we took the decision to maintain only one store (Vuex in your example) and give to each component a subset of this store according to its isolation. We have only one State and Mutators tree by app.
A component is just a way to add features to your app. But it is not either a completely independent feature. When we are talking about component, we are just talking about a Blueprint to add to the app, to give it more functionnalities, like a mixins but that could be composable and mutlipliable.
Thats why, I immediately jumped into the mixins logic to present my solution. Because components are super-powered isolated replicable mixins.
@MatejMazur I cant use Slack at work, yeah Slack is for work team, but my IT department consider it as "distraction". But gitter is fine :laughing:
@Swizz Let's go back to this comment for a second.
The problem I have with that is the Counter's actions:
actions: {
Counter: {
up: ({Counter: state}) => ({ Counter: merge(state, { [props.key] : merge(state[props.key], { count: state[props.key].count + 1 }) }) }),
down: ({Counter: state}) => ({ Counter: merge(state, { [props.key] : merge(state[props.key], { count: state[props.key].count - 1 }) }) })
}
},
How can we improve that?
@jbucaran This is because, with this naive implementation, it is possible to give fractal state but not to work with. This need to be done by the core when merging partial state to the main state tree.
@Swizz Can you write the example again using pseudo-code and call this your proposal? 🙏
Here we go : https://gist.github.com/Swizz/8437df6ac5ec4985e72e648d242f6a9f
This is a complexe but complete example. (no components list yet)
With component props initialisation, parent->child
and child->parent
communication, Fractal state, and namespacing.
@zaceno I definitely don't want to take over anything, I was only chiming in with my 2 cents, I think all the feedback in this thread is useful.
@Swizz I like that gist, but I still feel like we should have some kind of helper or pattern to handle the key/id and at least a pattern for how the state could be partitioned for components. Maybe we even make the helper generic enough that it can allow users to describe their own lookup() for a components scoped state?
Edit: smarter people than I have solved what I was cludging in that code lol, https://gist.github.com/dtipson/37e47734f25f2b639779#file-make-lenses-js
@Swizz Why are you using emit
? 🤔 😕
@andyrj This will allow components to use the state out of their isolation. And I think this is not a good pattern.
@jbucaran child->parent
comunication. The change
event call the function on the onchange
attribute.
We can also move into a parent
property on state
and actions
, if you are more comfortable on this than props/events.
@Swizz Using emit like that is only optional right? 😅
EDIT
It is obviously optional. Anyway, events are currently not scoped like actions are, so users using emit
like this need to be careful.
Would it be possible to make the emit
passed into Counter
to emit instead Counter.change
?
So, scoped events. 😨
@jbucaran Only useful if you want to use the events system to interact with your parent
@Swizz I personally find the way component is declared and then used is unintuitive. The declaration should be no different to how we declare actions or events.
I use JSX because the API allow it. But, you can simply use function :
app({
components: (state, actions, emit) => ({
Counter: Counter({
initialValue: 5,
unit: {state.unit},
onchange: value => emit('change', value)
})
})
})
or if you do not need anything from the state, actions, emit :
app({
components: {
Counter: Counter({ initialValue: 5, unit: 'monkeys' })
}
})
@Swizz But why do you have to initialize the component when declaring it?
@Swizz So, why not just simply:
components: { Counter }
Because I dont want either to do it into view. Like I said before, Components are in my mind super-powered isolated replicable mixins. Some bluprint with add features to the main app.
So, this initialisation is more than what to add to the State to initialize it. And also a way to isolate Component and add the ability to deal with an array of same components.
@Swizz So how do you pass props down to the component in the view?
Why do you want to pass props into the view ?
In this idea, the view role is only to receive the component view vnode result and add it into its children array. The component hook is the only one responsible do deal with component logic.
View is to make a presentation of the state and add some hooks to deal with actions : no more.
Why do you want to pass props into the view ?
I think is is more intuitive to do it in the view. Let me see harder 🔎 🤓.
The component hook is the only one responsible do deal with component logic.
Sorry, what is the component hook?
I dont know how to call these things ^^
app({
components: ...
})
I think is is more intuitive to do it in the view
You, are the boss. But I am still against :boxing_glove: 😄
They are called properties.
The following code looks like when you instantiate a component in React.
components: state => ({
Counter: (
<Counter initialValue={5}, unit={state.unit}, onchange={value => emit('change', value)}/>
)
}),
It's also not clear to me when is this function called and how many times is it called. I guess only once so it's intializing the view?
I think this can and will put off people, specially beginners or those coming from other libraries. I, myself, am very confused.
I am not clear about that, in fact. Initialisation maybe. But what about conditional component or rendering a list of components according to an HTTP request result ?
@Swizz What about them? I don't see an issue with that because the wrapped component view is passed to the application view in the function like:
view: (state, actions, { Counter, ... }) => ...
Let's discuss how components could look like in a future version of HyperApp.
Submitted Proposals
Example
counter.js
index.js
Credits
All credit goes to @MatejMazur:
Related
147
219