-
Notifications
You must be signed in to change notification settings - Fork 27
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
Not really PR but rather annotations and questions inline #31
base: master
Are you sure you want to change the base?
Conversation
Codecov Report
@@ Coverage Diff @@
## master #31 +/- ##
==========================================
- Coverage 94.14% 93.93% -0.21%
==========================================
Files 5 5
Lines 393 396 +3
Branches 62 62
==========================================
+ Hits 370 372 +2
- Misses 23 24 +1
Continue to review full report at Codecov.
|
Now removed the |
Thank you for showing interest in Turbine. I will try to explain some of the code with comments on the lines during the next day or two, but for now I will try to clearify this:
I see a component in Turbine as a container holding descriptions of:
Since components are just descriptions, we can do this: const btn: Component = button("Click me");
const view: Component = div([
btn,
btn,
btn
]); Here both <div>
<button>Click me</button>
<button>Click me</button>
<button>Click me</button>
</div> |
@limemloh Many thanks for your quick explanation! I have found my confusion was to having missed the I must be still confused about how the div(
function* () {
const
select1 = yield selectorButton("1", selected),
select2 = yield selectorButton("2", selected),
select3 = yield selectorButton("3", selected),
select4 = yield selectorButton("4", selected);
return { selectVersion: combine(select1, select2, select3, select4) };
}
) So the Playing with it in console, I got the feeling that the In the code, this Component is passed to the const versionSelector = modelView<FromModel, FromView>(
function ({ selectVersion }) {
const selected = stepper("1", selectVersion);
return Now.of([{ selected }, { selected }] as [FromModel, FromModel]);
},
function ({ selected }): Component {
return div(
{ class: "btn-group" },
function* () {
const
select1 = yield selectorButton("1", selected),
select2 = yield selectorButton("2", selected),
select3 = yield selectorButton("3", selected),
select4 = yield selectorButton("4", selected);
return { selectVersion: combine(select1, select2, select3, select4) };
}
);
... The second view function seems to use the In the counter example, version2, we have button({
// produce output {incrementClick: clickStream} ?
output: { incrementClick: "click" } },
" + "
), I am somewhat confused about the type of the output, does it match the Model's input? type CounterModelInput = {
incrementClick: Stream<any>,
decrementClick: Stream<any>
} So is it an object with Stream values?
From the usability perspective, array output would be painful, because every time you change the order of your elements, you have to adjust the types. Also the order of the elements reflects their order in the dom, which may be different from how you see the output ordered, if going this way. But I would still find the object output easier to use, and matching the input type of the Model would seem to be the easiest. Comparing to Redux, the output correspond to the actions, which in the Redux's case you have to mark with the action types, making them noisier. In case of turbine's outputs, combining them into the object right away can nicely solve this problem. That way the streams are also nicely separated, rather than merged like in Redux, so you don't need to react to unwanted events and filter through all foreign actions. As the syntax matter, I'd love it to make as terse as possible, e.g. button({
clickStream: 'increments'
}) So that would tell me, the However, I don't feel that way it is expressive enough, as it looks like ordinary object, button({
onclick: yield 'increments'
}) would instantly alert the reader that something unusual happens, which is good. There is some design problem though, namely, what if two buttons try to use the same prop: button({
onclick: yield 'increments'
})
button({
onclick: yield 'increments'
}) One solution would be to empower this way by merging both streams, So I see the The output passed to the function reducers(actions) {
const moveHighlightReducer$ = actions.moveHighlight$
.map(delta => function moveHighlightReducer(state) {
const suggestions = state.get('suggestions')
const wrapAround = x => (x + suggestions.length) % suggestions.length
return state.update('highlighted', highlighted => {
if (highlighted === null) {
return wrapAround(Math.min(delta, 0))
} else {
return wrapAround(highlighted + delta)
}
})
})
...
return xs.merge(
moveHighlightReducer$,
...
)
} https://github.com/cyclejs/cyclejs/blob/master/examples/autocomplete-search/src/app.js#L143 Which is perhaps a sign the design is good :) Now, pushing further the comparison, Cycle lets the reducer output a single stream, I would find it easier to keep the consistency But I would like to avoid the From the user's perspective, it would be simpler not to think about it, and following the same consistency principle, return an object of streams. Then it is up to its view sister and the parent component to each pick the right ones. Finally, this line is perhaps the trickiest to digest: const count = yield sample(
scan((n, m) => n + m, 0, changes)
); It seems to go through the stateful scan I understand that the This seems to resonate with what Andre @staltz recently wrote on his blog about Hot vs Cold, and how that caused some difficulty in Cycle, where the impure methods like the At least, this is my interpretation, and I feel it is worth to try to better explain things clearly what they are, why the extra complexity is needed, and how it helps to solve the problems. I find the Any corrections, answers to questions posed and more explanation will be greatly appreciated. |
This interesting discussion might be related: |
The
No, at the moment Turbine merges the objects like const btn: Component = button({output: {clickStream: click}}, "Click me");
const view: Component = div([
btn, // 1
btn, // 2
btn // 3
]); When
The implementation of but to hide away function scanNow(...args) {
return sample(scan(...args));
} but |
Thank you for the explanation, I think my confusion comes from the Mithril way, where every component must be wrapped with the element creator before included as child. And I like how the Turbine hides this complexity instead, so you can inline the component directly 😄
input().chain(
inputOutput => span(inputOutput.inputValue)
) I can see it as the easiest implementation to place it simply after the span(
// how can I get the input's output here?
),
input()
Then how would you go if you actually do want to merge them? Are there sufficient use cases to justify this complexity, without which the problem wouldn't even exist? :)
Oh yes, I have missed the double behavior type ;) Also the name |
Yes, the array overload will remove some power, but together with the const view = loop(({inputValue}) => div([
span(inputValue),
input()
]));
I came up with these ways: This is the one I would use in most cases, since the buttons probably would have different text const view = div([
button({output: {clickS1: click}}, "Click me");
button({output: {clickS2: click}}, "Click me");
button({output: {clickS3: click}}, "Click me");
]).map({clickS1, clickS2, clickS3} => ({clickStream: combine(clickS1, clickS2, clickS3)})); if the buttons actually was suppose to be identical we could do this: const btn = (name) => button({output: {[name]: click}}, "Click me");
const view = div([
btn("clickS1"),
btn("clickS2"),
btn("clickS3")
]).map({clickS1, clickS2, clickS3} => ({clickStream: combine(clickS1, clickS2, clickS3)})); But say that I was using a component from som library, and their API did not allow us to specify the name of the output, I would use a generator like this: import {btn} from "turbine-bootstrap";
const view = div(function*(){
const {clickStream: clickS1} = yield btn;
const {clickStream: clickS2} = yield btn;
const {clickStream: clickS3} = yield btn;
return { clickStream: combine(clickS1, clickS2, clickS3)};
}); But if we really wanted to use the array overload, we could do this: import {btn} from "turbine-bootstrap";
const rBtn = (name) => btn.map(({clickStream}) => {[name]: clickStream});
const view = div([
rBtn("clickS1"),
rBtn("clickS2"),
rBtn("clickS3")
]).map({clickS1, clickS2, clickS3} => ({clickStream: combine(clickS1, clickS2, clickS3)})); About whether it is mostly streams the components will output, I do not agree. Since Turbine components hold state mostly in behaviors and other components might depend on this state, the output would be a mixture of streams and behaviors. |
yes, indeed. There is another variation of
I think that hareactive's |
Thank you again for your answers! const view = loop(({inputValue}) => div([
span(inputValue),
input()
])); This feel somewhat "magic".
Thanks, interesting to see so many ways :) This would be the simplest possible syntax I could think of: const view = div([
button({ output: 'click' }),
button({ output: 'click' }),
]).map([clickStream1, clickStream2] => ({clickStream: combine(clickS1, clickS2)})); Would it make sense? BTW, you seem to rename
Yes, this point was well-made by @paldepind in his blog post :)
You are right, I was confused by the word "stateful". Now I am not even sure why it is call that way. Was adding the behavior part not meant to remove the statefulness?
The biggest difference is the different return type. It might cause some errors if you are not careful I think. Also the usual |
But, at a "theoretical level" it isn't magic. What If I understand correctly you've used Cycle. You know that the framework is based on a single global circular dependency chain. What we've realized is that circular dependencies happen very frequently in FRP. And that only having a single global circular dependency is inconvenient. Thus we provide So in a sense, each component created by |
LOL. I think what I'd like to see is how the const view = loop(({inputValue}) => div([
span(inputValue),
input({ output: { inputValue }})
])); That is, I like my components to make it clear what they want to output.
It is an interesting point of view. :)
I have tried to use Cycle and worked through the examples but hit several walls, with this one you mention being exactly one of them. Without even being clearly mentioned there. Which made me really love your diagram when I saw it in Turbine :) |
Here are some points about the |
This is not meant a PR but rather a result of my understanding attempt
with annotations and inline questions caused by some confusion,
that I would like to share this way.
Especially I'm puzzled how this inline plain object generates a vnode:
https://github.com/dmitriz/turbine/blob/annotate/examples/todo/src/TodoApp.ts#L79
Would be great to hear your feedback on these and other questions/remarks.
Sorry for the generated
.js
files that unfortunately got committed for some reason,not sure whether it was intended, please ignore them here.