funkia / turbine

Purely functional frontend framework for building web applications
MIT License
685 stars 27 forks source link

Confused by all the output options #87

Closed stevekrouse closed 5 years ago

stevekrouse commented 5 years ago

I'm very confused by the various styles of output:

// 1
const counterView = ({ count }) =>
  div([
  "Counter ",
  count,
    button("+").output({ incrementClick: "click" }),
]);

// 2
const counterView = ({ count }) =>
  div([
    h2("Counter"),
    count,
    button({ output: { incrementClick: "click" } }, "Count")
  ]);

 // 3
const counterView = go(function*({ count }) {
  const {click: incrementClick} = yield button("Count")
  yield text(count)
  return { incrementClick }
})

Are some older styles that still work or are some fully depreciated? It'd probably go a long way towards my sanity if I knew which was the preferred way. (Unless they are not fully equivalent in which case I'd be curious to know the trade-offs of each style.)

paldepind commented 5 years ago

Option 1 and 2 does exactly the same. In the latest version of Turbine option 2 was removed. Essentially all the inbuilt HTML element functions in Turbine accepted an output property on their input object with the same behavior as using the output method on components. We removed it to

As for option 3. Using generator function corresponds to invocations to the chain method (similarly to how do-notation in Haskell corresponds to invocations of the >>= operator). We've tried to explain it in the documentation. If that is unclear please let me know. With that in mind I think the question is: what is the difference between the output method and the chain method? I think that will take a slightly longer explanation. I will get back to you with that :smile:

stevekrouse commented 5 years ago

Ok, so version 2 is gone and dead. Are 1 and 3 equivalent?

I understand that chain is bind (>>=) in Haskell or then in JS. So how does output correspond to Haskell?

I guess this relates to my question #93 on go and fgo

paldepind commented 5 years ago

Option 1 and 3 are not equivalent.

Option 3 is very similarly to what Reflex does, it allows for creating views using do-notation (or in our case the go function) and it supports combining model and view.

Option 1 is unique to Turbine and works well when separating model and view. Option 3 can also be also be used when separating model and view, and we did that in the beginning, but option 1 is ideally suited for this use case. My own opinion is that combining model and view and using option 3 is in most cases a bad idea. But before we can properly have a conversation about that I should explain option 1 properly.

In order to understand the output method it's important to understand the difference between selected output and available output. This is currently not documented at all, thus I've created the PR #99 to document it. Here's a link to the updated section in the PR that hopefully does an adequate job at explaining it.

The takeway of the added documentation in #99 is this: The output method moves output from the available output into the selected output. When a view is build all the selected output is merged an it becomes the final output.

This makes it very easy to build views with nesting with nested elements. It doesn't matter if a button is heavily nested in some other HTML. It's selected output will "bubble up" as the button is combined with its siblings and parents.

I think the following example would be very annoying to implement using Reflex or option 3 since the a elements are nested so deep.

const counterModel = fgo(function* ({ incrementClick, decrementClick }) {
  const changes = combine(incrementClick.mapTo(1), decrementClick.mapTo(-1));
  const count = yield sample(scan((n, m) => n + m, 0, changes));
  return { count };
});

const counterView = ({ count }) =>
  div([
  "Counter ", count,
  div([
    text("Here is the first button"),
    div({class: "button"}, [
      a({ class: "btn btn-default" }, "+").output({ incrementClick: "click" }),
    ]),
  ]),
  div([
    text("Here is the second button"),
    div({class: "button"}, [
      a({ class: "btn btn-default" }, "+").output({ incrementClick: "click" }),
    ]),
  ]),
]);

const counter = modelView(counterModel, counterView);

Maybe Reflex has some way of handling this, but as far as I'm aware doing the above is quite boiler plate heavy as one has to write code at every level to get the output to "bubble up". Option 1 in Turbine does this seamlessly.

Let me know if the added documentation in #99 makes it clear how output works an what it achieves. Otherwise I'll try to explain it more deeply.

paldepind commented 5 years ago

Did the explanation above help you @stevekrouse?

stevekrouse commented 5 years ago

This all makes sense. I sometimes have trouble getting Option 3 working correctly, but I guess that's less recommended/supported....

paldepind commented 5 years ago

It seems that this issue can be closed now. Option 2 has since been removed in favor of always using option 1 instead. So I guess that improves the situation. Option 3 still works but it's probably more advanced usage and probably not something that a new user would have to even know about.