-
Notifications
You must be signed in to change notification settings - Fork 781
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Components #238
Comments
Yesterday I've created partially (see issues below) working solution without Here is actual and working variant of writing apps with this version: Counter examplewith initial values
|
This one looks too magical. For someone like me who is new into the Hyperapp community. I am pretty in love about the fractal idea for state/actions. The main concern is to handle list of same component with their own state. |
@Swizz Thanks for your answer. 👍 But I think that only magic is ID part (we need some identification for instances of component if we want keep single source of truth but maybe we can hide it somehow). If you look closer. You will see that component |
@matejmazur The magical part is what you are doing inside The reason it is magical is that custom tags return a virtual node tree, not an object. So, although I like this I also agree with @Swizz. idsI don't think we need to get rid of ids. The id is not really the core of how components work, but rather how index.js keeps track of them. Let's keep thinking. I've updated my example to fill in the missing code. |
@matejmazur How would you pass initial values / props to a component in your example? |
Awesome @matejmazur. Looks like a good start. A clean solution to me. Like @jbucaran, I'm wondering how you get more control over the props and children. I do see you pass them through What if you did the exact same thing, but wrapped it in a function that was passed const Component = (props, children) => ({
// ...
}) Instead of passing them through every single avenue like with state/actions. Do you think this would work? |
FWIW, we recently had the same discussion in domvm [1], since it works this way. the only issue is that if either of those values ever get replaced by a parent, they become stale in the closure. |
state: {
...,
counters: [
{ // props are just everything inserted to custom tag
someValue: 0,
someText: 'foo',
initial: 1 // I want to keep eye specifically on "initial" because everytime you update it,
// you'll get to the problem with previous state of component...
// what to do in such situation? ..reset state?
// I think that it's important to keep such important value separately
// from props inserted only to view part of component which don't
// affect component state but hey..we can change that :-D
}
]
}
...
{state.counters.map((data, index) => (
<Counter {...data} /> // variant A
<Counter
value={data.someValue}
text={data.someText}
initial={data.initial} /> // variant B
))}
Send example of some situation where you need more "open access". Now the
I think that it's important to keep ordinary props separately from component state and actions because if you want somehow to interfere to component actions then why would you do that? You can change behavior of component actions by initial value (it can be str, num, obj, arr) inserted in component state then you can use it through component state in args of action. The same way as you do these things in |
FWIW, with one really tiny change in ... of course this approach breaks the "single source of truth" notion. In my setup each component is it's own app with it's own state. Communication between parents need to happen via props (pass notification actions down to children). So it's not the perfect solution. (For example, time-travel debugging a bunch of apps that can come and go through the life of an app seems pretty tough, if not impossible) But I thought it relevant to mention in this thread anyway. |
Hi everyone. I saw this thread after I already had some expectations as to how components would work and I thought I'd show you what they were. In a nutshell:
See #245 |
@naugtur Thanks for the feedback. This thread is to speculate and discuss how the component API could look like.
Absolutely! 💯 |
My contribution to the discussion, with a little bit of my Cycle.js thoughts (Onion, Isolate, etc...) Here is a working example on Hyperapp 0.9.3 🎉
|
As #238 is on the way, mixins may be removed or its behavior may change.
A little bit more neat with JSX. 👍
|
I would like to jump into this conversation by saying that I really like where this little framework is going. I have been using Elm for some time now and the best thing for me is that Elm is elegant and simple to use. Its syntactically beautiful. As Hyperapp is trying to be Elm-like I guess seeing those objects nesting one into another just tooks all the beauty. Why we cannot use new Javascript features to have beautiful looking framework. In my opinion, getting things out of those objects and at least letting components have a nice syntax would be a great decision. I have been working on my app and wrote some helper functions and classes to help me work with it. Here is the simple component which I thing looks simple and easy understand what its doing by just looking at it. class Counter extends Component {
constructor(props){
super(props);
}
render(){
return (
<div>
<button onclick={this.actions.add}>+</button>
{this.state.num}
<button onclick={this.actions.sub}>-</button>
</div>
);
}
}; Yes it looks like `React but its the syntax people have been using for a long time right now, and they like it. Projects always keeps growing and having a nice way to write components will make our lives just easier. This is only my opinion. Keep up with a great work 👍 |
Classes bring side effects and all the mess were avoiding with functional programming. It's all about references to functions and the main structure of the app could be declared once while each component is a pure function. That's what came with elm, classes would hurt that. |
@Dohxis We want a single state architecture. So, using |
I can understand that classes would bring unwanted things, did not experienced on my small app though. But I am keeping single state and single actions. They are passed as props. |
I was thinking about how to implement interactive doc, when I was punch by an awesome thing : <body>
<app-counter></app-counter>
<app-counter></app-counter>
<app-counter></app-counter>
</body> const { h, app } = hyperapp
const Counter = (props, children, element=document.body) => ({
root: element,
state: {
value: props.value
},
view: ({ value }, { inc, dec }) =>
h('div', { class: 'counter' }, [
h('button', { onclick: inc }, '+'),
h('span', null, value),
h('button', { onclick: dec }, '-')
]),
actions: {
inc: state => ({ value: state.value + 1 }),
dec: state => ({ value: state.value - 1 })
}
})
document.querySelectorAll('app-counter').forEach((elem) => {
app(Counter({value: 0}, [], elem));
}) This is not really great to see ; but it works well : app({
state: {
title: "Hello."
},
view: (state, actions) =>
h('main', null, [
h('h1', null, state.title),
h('app-counter', null)
]),
events: {
render(state, actions, view, emit) {
document.querySelectorAll('app-counter').forEach((elem) => {
app(Counter({value: 0}, [], elem));
})
}
}
}) PS : Im doing a lot of experiments with hyperapp to use it into a large scale project, if you find my every day comments here a lot annoying, do not hesitate to ask me to stfu. |
@Swizz Please keep them coming! It adds a lot to the discussion. 🙏 |
We could also think about adding Example app({
state: {
title: "Hello."
},
data: {
currentView: 'home'
},
components: {
home: { doSomethingHere },
About: { doSomethingHere },
Contact: { doSomethingHere }
}
}) So depending upon the UI logic we could switch between different component using a wrapper or a method. This would be useful in doing transitions (transitioning between the components or elements) and other stuff. |
I think is is more intuitive to do it in the view. Let me see harder 🔎 🤓.
Sorry, what is the component hook? |
I dont know how to call these things ^^ app({
components: ...
})
You, are the boss. But I am still against 🥊 😄 |
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, ... }) => ... |
@Swizz What about this? const Counter = (props = { initialValue: 0, unit: "" }, children) => ({
state: {
count: props.initialValue,
unit: props.unit
},
actions: {
inc: (state, actions, data, emit) =>
emit("change", { count: state.count + 1 }),
dec: (state, actions, data, emit) =>
emit("change", { count: state.count + 1 })
},
view: (state, actions) =>
<div class={"counter"}>
<button onclick={actions.inc}> + </button>
<span>{state.count} {state.unit}</span>
<button onclick={actions.dec}> - </button>
</div>
})
app({
state: {
title: "Best counter ever",
unit: "monkeys"
},
view: (state, actions, { Counter }, emit) =>
<main>
<h1>{state.title}</h1>
<Counter
initialValue={5}
unit={state.unit}
onchange={value => emit("change", value)}
/>
</main>,
components: { Counter },
events: {
change: (state, actions, value) => {
alert(value)
}
}
}) |
How can I use 3 counters ? An a unknown number of counters based on an initialValues array ? |
@Swizz Is that not a problem also in your example? |
I am not highlighting problem, I am asking about your thought on it ^^ components: state => ({
Counters: [
<Counter initialValue={1} unit={state.unit} onchange={value => emit('change', {i: 1, value})}/>,
<Counter initialValue={2} unit={state.unit} onchange={value => emit('change', {i: 2, value})}/>
<Counter initialValue={3} unit={state.unit} onchange={value => emit('change', {i: 3, value})}/>
]
}), components: state => ({
Counters: state.initialValues.map((value, i) =>
<Counter initialValue={value} unit={state.unit} onchange={value => emit('change', {i, value})}/>
)
}), With your view: (state, actions, { Counter }, emit) =>
<main>
<h1>{state.title}</h1>
<Counter initialValue={1} unit={state.unit} onchange={value => emit('change', {i: 1, value})}/>
<Counter initialValue={2} unit={state.unit} onchange={value => emit('change', {i: 1, value})}/>
<Counter initialValue={3} unit={state.unit} onchange={value => emit('change', {i: 1, value})}/>
</main>, view: (state, actions, { Counter }, emit) =>
<main>
<h1>{state.title}</h1>
{state.initialValues.map((value, i) =>
<Counter initialValue={value} unit={state.unit} onchange={value => emit('change', {i, value})}/>
)}
</main>, Both works the same, the main purpose is all about, the power we want to give to the view property. My mind in stucks about separation and isolation ; your is on simplicity and intutivity. |
@Swizz What is unit by the way? And won't we have to provide an index to the component? |
Unit is just another attribute to illustrate parent->child communication and the index is only for the events example. The id key could be set randomly or by given a key to the component like it does for vnode. |
@Swizz What do you think of a random id? EDIT: Not an actual #id or a keyed-dom key, but a unique identifier among siblings. |
Not pure but easy to use and to think about |
What would be a pure way to do it then? |
|
@Swizz Just to be clear, we're not talking about keys as in keyed-vdom keys. Right? |
The purpose could be the same "identify a thing". But the implementation is différente. The only purpose here is about isolation. |
God this is long. I'm in a different timezone apparently. I'm half way through and I need to comment on the image from @Swizz I really don't think the state should be built based on the parent-child relationships of components. State is the most important part of the architecture and I (as an app developer) will never give up my decisions on the data strucutres used in state. State should be structured the way developer wants and subtrees of state passed to components consciously. Anything else is a guaranteed disaster. There are 3 sane ways to choose from, to pass a fragment of state into a component in order to scope it:
Second option is the simplest, but it's based on a convention and might be easy to break. Option 3 is implemented in my fork. https://github.com/naugtur/hyperapp/blob/master/poc/index.js |
Good point about state structure being sacred to the developer, @naugtur I'm not familiar with the connect-pattern from redux react, so let me check if I follow your poc correctly: The "context" available to "connected" components, is defined in the Is that correct? That doesn't seem all that different from my proposal. The one difference I can see is that you have the provider defining what the context is, whereas I just bind the apps full state and actions to components. Do you agree or disagree? Also like my proposal (well almost), it seems yours does not require changes in the app or h functions, and could be used via a separate package. True? |
@naugtur In your example, the app has the actions that can up and down the counter. If that's going to be always the case, then what is the advantage of your controls/components from using custom tags? |
@jbucaran actually, I wouldn't want them to be much more than custom tags. Components (in my view) are custom tags that you don't have to pass all properties to, at least not explicitly. |
@naugtur So they are custom tags that are bound to the state and actions? |
Yes. I would add limiting scope of state passed in, but overall it's what I'd like to use. |
I am having mixed feelings about components and I am not even sure we should add a new core concept to the framework. I am not even sure we even have a problem. I say, let's build one or two cool apps first and then revisit this issue. 🤔😅😎 Now, I'm still interested to see @naugtur's #245's final form and the door is open to anyone who wants to add more to this discussion. I am glad this conversation happened, though, because it motivated me to revisit hyperapp core values and I can confidently say I like it now more than ever. To summarize my feelings about components:
But nothing is lost. On the contrary, we've gained the insight that there exist patterns that can help you create complex applications without losing the benefits of a single state tree. PatternsWidgets@zaceno's state/action bound custom tags is interesting and could be published as an independent module. I like to think of them as "widgets". 🤓 See: https://codepen.io/zaceno/pen/gRvWPx Split-mixins@matejmazur's Split-mixins is a clever trick that requires no changes to core and lets you use mixins to achieve something similar to stateful components. const MyComponent = ({ state, view, actions }) => view
? ({ state, actions, events, mixins }) // good ol' mixin
: <main>...</main> // a custom tag Mixins@Swizz Super-powered mixins. See: #238 (comment). Other
Final remarksWe're all (I am!) on a learning curve and tyring to figure this out. I am sure more patterns will show up, so please share your research and findings. Thank you all, for your feedback and such an epic thread! 💥 🎉🥇 |
Here is package for mixins with view (it's just wrapper for my trick which @jbucaran mentioned in this comment. With this you can write mixins like this: const Counter = mixin({
view: (state, actions, children) => (
<div>
<div>Number of counters: {state.length}</div>
<button onclick={actions.addCounter}>Add counter</button>
{state.map((instance, index) => (
<div key={instance.id}>
<button onclick={() => actions.increase(index)}>
Increase
</button>
<span>{instance.sum}</span>
<button onclick={() => actions.decrease(index)}>
Decrease
</button>
<button onclick={() => actions.removeCounter(index)}>Remove</button>
</div>
))}
</div>
),
state: {
counter: R.times(() => initial(), 2) // default number of loaded counters
},
actions: {
counter: {
addCounter: (state) => ({
counter: R.append(initial(), state.counter)
}),
removeCounter: (state, actions, index) => ({
counter: R.remove(index, 1, state.counter)
}),
increase: (state, actions, index) => R.over(
R.lensPath(['counter', index, 'sum']),
R.inc,
state
),
decrease: (state, actions, index) => R.over(
R.lensPath(['counter', index, 'sum']),
R.dec,
state
)
}
}
}) and then it can be used in app like this: app({
view: (state, actions) => (
<Counter state={state.counter} actions={actions.counter} />
),
...,
mixins: [Counter]
}) That's it! |
As #238 is on the way, mixins may be removed or its behavior may change.
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
The text was updated successfully, but these errors were encountered: