Giter Site home page Giter Site logo

funkia / turbine Goto Github PK

View Code? Open in Web Editor NEW
684.0 26.0 27.0 4.61 MB

Purely functional frontend framework for building web applications

License: MIT License

TypeScript 99.65% JavaScript 0.35%
typescript functional-reactive-programming framework pure javascript frp

turbine's People

Contributors

deklanw avatar dependabot[bot] avatar dmitriz avatar fbn avatar j-mueller avatar jkzing avatar jomik avatar limemloh avatar paldepind avatar stevekrouse avatar vrobinson avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

turbine's Issues

Single-page documentation

Documentation in a single page, such as lodash, is amazing.

Part of the appeal of Turbine is that it's built modularly on top of Harereactive, IO, and jabz, but it's super annoying to need 3-4 documentation pages open and have to flip between them, ctl-f-ing on each page. It'd be much better if we could ctl-f on a single page for what we need.

However, I wouldn't want to create an extra place you need to maintain documentation for all 4 projects. I wonder if there's some way to write documentation in each project and the automatically pull the documentation for each version into a centralized tool... I wonder what other projects with this problem do

Needs a sweet logo

Since it is about time to start focusing a bit on the documentation, I think it is important to have a sweet logo. I have almost no experience with designing logos, but gave it a try:
14627919_10210844392091715_2057334774_n
I don't find this good enough as Funnel's logo, but I'll let it be the first suggestion.

filterApply undefined behavior bug

filterApply(Behavior.of(undefined).map(() => () => true), stream).log()

Upon an event from stream errors: predicate.at(...) is not a function.

If you replaced undefined with 1, the error goes away.

Tree shaking

This issue is to keep track of the state of tree shaking in Turbine.

We want to support tree shaking in Turbine and it's dependencies (Hareactive and Jabz). The end goal is that when users build their applications the bundle should be as tiny as possible. Users should only pay in "bundle size" for the things that they actually use. This makes it possible to add as many useful functions as possible without worrying about bloat.

Much of the necessary steps have already been taken. All our libraries ships with builds that uses the ES2015 module format which should allow for tree-shaking.

However, it is currently not possible for neither Webpack or Rollup to shake away classes as TypeScript compiles them. This makes tree-shaking almost useless as we use a lot of classes internally. The following issue microsoft/TypeScript#13721 should fix this problem.

Embedding view into model?

I had another look at this inspiring diagram
https://github.com/funkia/turbine#adding-a-model
and got the feeling that the model really behaves like the parent to the view.

The view's both input and output only go through the model,
which exactly mirrors the child-parent component relationship:

// passed only inputs for the model
const model = ({ inputsForModel }) => {
    ...
    // the model is passing only what the view needs to know
    const view = ({ message }) => {
        return div([
            message ? `Your message is ${message}` : `Click button...`,
            button({ output: {updateMe: 'click' } }, `Click me!`)
        ])
    }
    ....
    // prepare message to show
    const message = updateMe.map(getMessage)
    ...
    // return the view with message updated
    return view({ message })
}

It looks like the model "knows" too much about view,
but I'd argue that it computes and passes the message exactly the same way as if the view were external. In fact, you could enforce it by rewriting your model as

const model =  ({ inputsForModel, view }) => ...

const view = ({ message }) => ...

const modelView = (model, view) => (...args) => model({ ...args, view })

and then reusing the same model with different views.

The advantage of this is enforcing more separation of concern,
where the view receives and emits only what is needed,
which need not be passed to the outside parents.

Also it may help to keep the view "dumb" and simple,
just passing actions and displaying the current state.
That would allow to simplify the structure by possibly entirely moving
streams and generators out of the view into the model.

It doesn't mean to use this pattern for everything,
but it may help simplifying many use cases.

What do you think?

Error message in modelView

There should be an error message if model doesn't return what view needs.

const model = ({a, b}) => {
  ...
  return Now.of({c, d});
}
const view = ({c, e}) => div(...);
const comp = modelView(model, view)();

here I expect an error, because the model doesn't return e

Take the localStorage out as parameter?

Just an idea,
motivated by the discussion in https://github.com/uzujs/uzu/issues/6#issuecomment-304519136,
maybe taking out the local Storage here as external parameter would make the data flow more explicit:
https://github.com/funkia/turbine/blob/master/examples/todo/src/TodoApp.ts#L69

Somehow I feel it does not really belong to the main app but comes from outside.

It might be interesting to compare with the implementation here:
https://github.com/briancavalier/most-todomvc/blob/master/src/index.js#L75

Why Turbine doesn't use virtual DOM

@dmitriz

Don't use virtual DOM. Instead, the created view listens directly to streams and updates the DOM accordingly. This avoids the overhead of virtual DOM and allows for a very natural style where streams are inserted directly into the view.

Could you explain this?
I've thought the point of virtual DOM was to avoid the overhead of the real one :)

In my opinion, virtual DOM solves a problem that we can completely avoid by using FRP.

Note: In this post, I'm using the words "stream" as a catch-all term for what different libraries call "stream" or "observable" and for what Turbine calls a "behavior".

Frameworks that use virtual DOM almost always represents their views as a function that takes plain state and returns virtual DOM. For instance, the render method on a React component is a function that returns a vnode tree from the components state. This is nice because it means that our view doesn't need to know which parts of our state changed and figure out what changes it then has to make to the DOM. It can just take the entire state and build a new virtual DOM. Then the virtual DOM library will find the differences and figure out how to precisely update the DOM.

When we use FRP the situation is quite different. We represent our state as streams. We can observe them and precisely know when pieces of our state changes. Based on this we can know exactly what changes to make to the DOM. We don't need a virtual DOM library to figure it out.

What I see almost all FRP/observable based frameworks do instead is something like the following: They start out with a bunch of different streams in their model, they then combine those into a single state stream and finally map their view function over the stream. So they go from state in many streams to state in a single stream to a stream of vnodes.

One very minor immediate downside to this approach is that the user has to write a bit of boilerplate code to merge the streams. Flimflam cleverly eliminates this boilerplate by automatically merging streams from an object.

The much bigger issue to the approach is that it throws information away that it then later has to get back by using virtual DOM diffing. Not only that, throwing the information away has a small overhead (the merging) and getting it back has an even bigger overhead (virtual DOM diffing).

To understand the problem let me give an example. Assume we have a model with three values, A, B and C. We express all of these as streams. We want a view with three p tags that each show one of the values.

<p>value of A here</p>
<p>value of B here</p>
<p>value of C here</p>

Whenever one of the streams changes we will have to modify the content of the p tag that it belongs to. What the above mentioned approach will do is something like the following.

const stateStream = combine(aStream, bStream, cStream);
const vnodeStream = stateStream.map(viewFunction);
// apply virtual DOM diffing to vnodeStream 

Note that when we have three streams we can observe each one of them individually and know when they change. But, when we combine them into a single stream we can only observe when any of them changes and don't know which one.

Therefore, when we apply our view function to the combined stream, it too doesn't know which value changed and it will calculate an entirely new vnode tree. Then the virtual DOM library will compare the new vnode tree with the last one it saw. It will then figure out if it was A, B or C that changed and update the view accordingly.

The virtual DOM library goes through the entire diffing process to figure out whether A, B, or C changed. But, we had that information originally before we combined the streams! If we hadn't merged the streams in the first place we wouldn't have needed that at all.

A smart enough view could simply know where each stream belongs in the DOM, observe that stream and update the DOM whenever that one changes. That is what Turbine does. In the above example, all three streams would be given to the view function and they would be inserted directly into the data structure describing the view. Then when the view is created Turbine will do something equivalent to the following

aStream.observe((a) => update first p element);
bStream.observe((a) => update second p element);
cStream.observe((a) => update third p element);

This updates the DOM just as efficiently as what a virtual DOM library would do. But it eliminates the overhead of virtual DOM diffing completely. And virtual DOM diffing does have an overhead. As an indication of that, consider React Fiber. It's a very complex solution to solve, among other things, the problem that virtual DOM diffing can in some cases be so expensive that it takes more than 16ms and causes frames to drop.

We don't have any benchmark yet. But we think the approach Turbine takes can give very good performance. In particular in cases where virtual DOM is ill-suited. I.e. animations and similar things that have to update frequently.

For instance, if you want to implement drag-and-drop in Turbine. We can represent the position of the mouse as a stream and hook that stream directly into the dragged element. Then whenever the mouse moves we will update the element position very efficiently.

To sum it up. Turbine doesn't use virtual DOM because it is unnecessary thanks to FRP. And since not using it leads to a concise way of expressing views where streams do not have to be combined but where they instead can be inserted directly into the view.

Please let me know if the above explanation makes sense and what you think about the rationale.

time.log() fails silently

It should at least error if it's not going to show you anything, but I might prefer it to show me all the milliseconds.

Setting inline style?

I have tried to set {style: {color: 'blue'}} inline in the simple example:

  yield div(
  	{style: {color: 'blue'}}, 
  	[
    	    "The address is ", 
    	    map((b) => b ? "valid" : "invalid", isValid)
  	]
  );

which worked but generated some errors in the console:

ERROR in ./index.ts
(13,4): error TS2345: Argument of type '(string | Maybe<"valid" | "invalid">)[]' is not assignable to parameter of type 'Child<string | Maybe<"valid" | "invalid">>'.
  Type '(string | Maybe<"valid" | "invalid">)[]' is not assignable to type '() => Iterator<Component<any>>'.
    Type '(string | Maybe<"valid" | "invalid">)[]' provides no match for the signature '(): Iterator<Component<any>>'.
webpack: Failed to compile.

Despite of the last statement, the color was actually changed (after the necessary manual reload).


On a more general note, you seem to be using the new custom element library. Would it work with snabbdom and other libraries?

On even more general note, I wonder what be your opinion on https://github.com/uzujs/uzu/issues/2

Hide `go`

Hide the go function so that generator functions can optionally be supplied directly.

Constructors in the API documentation

I find constructors very useful, particularly in creating mock code or to do sanity checks to test what's wrong, yet the only way I found them is by going into the test/ of each project. I would love these to be in the documentation for each project:

Behavior.of
Future.of
IO.of

(Let me know if you'd like me to tackle this one.)

not an issue - question about purity of main

Referring to the first example :

const isValidEmail = (s: string) => s.match(/.+@.+\..+/i);

function* main() {
  yield span("Please enter an email address: ");
  const { inputValue: email } = yield input();
  const isValid = email.map(isValidEmail);
  yield div([
    "The address is ", map((b) => b ? "valid" : "invalid", isValid)
  ]);
}

// `runComponent` is the only impure function in application code
runComponent("#mount", main);

and this comment // runComponent is the only impure function in application code;

To check that main is a pure function, I wanted to look at its inputs vs. outputs and possible effects it might do but I feel perplexed. How would you go about it?

If I understand main as being a generator function which return a generator object, then it comes down to the question of how to compare two generator objects, which is the same as testing two iterators for equality, which is a similar question to comparing two functions for equality, which is seemingly undecidable in the general case (cf. https://stackoverflow.com/questions/1132051/is-finding-the-equivalence-of-two-functions-undecidable) - it is obviously an easy problem if the function's domain is finite -- that is not the case here.

Zooming in on function equality, note that defining two functions as equal iff their function.toString() is the same, i.e. if they have the same source code, does not lead to interesting properties. The general understanding of function equality is that f === g if for every input x of f's domain, f(x)=g(x). There I am talkign about function who do not perform effects, if they do, the definition could be extended to include the fact that they perform the same effects (and that extends the problem to defining equality on effects...).

If similarly we define equality of generator objects as generating the same sequence, then it does not seems that two execution of main will give the same generator object. That will depend on the value entered by the user.

Where did I go wrong?

HTML to Turbine?

Although I like the concept of this a ton, I really think HTML is easier for visually understanding a UI hierarchy.

Would it be possible to do something like what Vue does (or JSX, though I like Vue better), to convert HTML-like templates into Turbine JS components?

Reactive Magic

@ccorcos

This is an answer to paldepind/flyd#142 (comment).

Again, thank you for the feedback and thank you for taking the time to take a look at Turbine. It is really useful to get feedback like that and I'm grateful that you're shaing you opinion ๐Ÿ˜„

Spent some more time looking through Turbine this morning... That's some pretty intense stuff! It's very well thought-out, but building a mental model for how it all works is pretty challenging. I think if you included explicit type annotations in the tutorial, it would be a lot easier to pick up on. I think it might also help me understand how everything works if you had an example that showed me how to get all the way down to the actual DOM node where I could do things like call scrollTo() or instantiate a jQuery plugin or something.

For using a jQuery plugin we'd probably have to create a mount hook that gives the raw element. We haven't done that yet though. Regarding scrollTo I'll get back to you with an example.

I see what you mean about the differences though. It actually is a bit different. No selectors is ๐Ÿ‘ and the code is really clean. I'm still trying to figure out where the challenges will be...

Let me know if you figure it out ๐Ÿ˜„. We have tried to make Turbine as powerful as possible. There are some functional frameworks that achieve purity by limiting what one can do. In Turbine we have tried to create an approach that is pure but without making things harder.

If you had a global application state for sidebarOpen that you needed to access in many places, I'm assuming this would just be a behavior that you can just import and combine in a model function? It wouldn't be pure though, right?

Turbine is completely pure. The answer to the question "is it pure" should always be "yes". sidebarOpen would probably be created inside a component and then the component would have to pass it down the children that need it.

You wouldn't be able to just import it. We can do that with a few behaviors. For instance, the mouse position, the current time and keyboard events can simply be imported. That is because they "always exist" in the browser. But sidebarOpen would be created inside some component so it can't be a global behavior that can be imported.

To stretch this abstraction even further, I might want to have two counters: the first counter has a delta of 1, and the second counter has a delta of the value of the first counter. Here's how I would do it using reactive-magic:

That is a good example. Here is how one could write that using Turbine.

const counterModel = go(function* ({ incrementClick, decrementClick }, { delta }) {
  const increment = snapshot(delta, incrementClick);
  const decrement = snapshot(delta.map(n => -n), decrementClick);
  const changes = combine(increment, decrement);
  const count = yield sample(scan((n, m) => n + m, 0, changes));
  return { count };
});

const counterView = ({ count }) => div([
  button({ output: { decrementClick: "click" } }, "dec"),
  span(count),
  button({ output: { incrementClick: "click" } }, "inc")
]);

const counter = modelView(counterModel, counterView);

const app = go(function* () {
  const { count } = yield counter({ delta: Behavior.of(1) });
  yield counter({ delta: count });
});

You can check out the example live here.

What intrigues me so much about this example is how clean the mental model is. It feels very easy to make sense of to me.

I think your example is nice. But, I think the one I wrote in Turbine is even better. I think the Turbine code avoids some problems. Problems that I see in many frameworks and some we've particularly tried to avoid in Turbine. Here are some of the problems.

  1. The definitions lie. For instance, the line delta = new Value(1) doesn't actually tells me what delta is. It says that delta is equal to 1 but that obviously isn't the entire truth. This means that if I want to know what delta actually is I'll have to find all the lines that use delta. In a real app that can be hard.
  2. When I look at the inc method I cannot see who may call it. It might be called once, twice, or many times in the view. This makes it hard to figure out when exactly the side-effect that inc has is triggered.
  3. The input to counter is mixed with the output from the counter. For instance, this line:
<Counter count={this.delta} delta={1}/>

It looks like both count and delta are input to the component. But, since Counter calls
update on count it actually uses it as an output channel. This means that whenever a Value instance is passed to a component it's hard to know if it's actually input or output.

I apologize for being a bit hard on your example ๐Ÿ˜… The problems I pointed out are found in most React code. Let me explain how Turbine avoids them.

  1. In Turbine definitions always tells the entire truth. Every time a line begins with const the definition tells everything about the defined value. This makes it very easy to look at the code and figure out what things are.
  2. There are not impure methods in Turbine. You never looks at something and wonder "who are actually triggering this code?". The code always clearly describes where things are coming from.
  3. In Turbine a component function takes all its input as arguments and all its output is part of its return value. This makes a lot of sense. Input is arguments to functions and output is returned from functions. For instance, in the Turbine example, counter is a function that returns Component<{count: Behavior<number>>}.

I think the properties I described above makes Turbine code easy to understand and will make it scale really well with increasing complexity.

Simplify the counter example?

This seems to look like the case for the loop:
https://github.com/funkia/turbine/blob/master/examples/counters/src/index.ts#L44

That would remove the need for the model, right?
There is the conversion from stream into behaviour in the model,
I see where it comes from, but having one thing for both might possibly be simpler here?
Then it would be just one short loop :)

The component itself looks like a general purpose selector component,
to which you pass the array labels = [1, 2, 3, 4],
along with the child component selectorButton.

I wonder if that can be made into something nice and reusable...

Usage of OOP Classes

The OOP subject came up in this discussion and I have been wondering myself about the reasoning to use the OO classes over FP factories, where several class-style declarations and annotation would not be necessary, as far as I understand.

I am sure you guys thought well through it, just curious what were the ideas behind it.

Is combining model and view possible?

I know there are a lot of other issues discussing the pros and cons of separating the model and view. I'm simply wondering if it's currently possible to do given the current architecture, and if so, if someone could give me an example of it.

My motivation is that I like how in Reflex you can combine the model and the view seamlessly, but also separate them if you want. I like the extra freedom. The modelView function and the architecture it imposes feels a bit restrictive to me.

New name for `component`

The component function needs a better name.

A few ideas from the top of my head

  • createComponent
  • modelView
  • createModelViewComponent
  • mvComonent
  • statefulComponent
  • controllerComponent

Devtools

This issue is the place for brainstorming and discussing devtools for Turbine and Hareactive. Some of the features that such a devtool may include are

  • Visualizing the dependency graph between streams and behaviors.
  • Making it possible to inspect the history of streams.
  • Time travel.
  • Manually pushing occurrences to streams and changing behaviors.

Server Side Rendering

That would be awesome to do with Turbine. Maybe we can just run turbine stuff in jsdom, and get the output?

Maybe there could be a way to make Turbine output strings instead of actual DOM elements?

Versioned documentation

I wasted a lot of time today trying to use input because that's what it is in the current README on Github, but that's a brand new change, so the version I was using had it calledinputValue.

Is the proper way to read Turbine documentation to go to the README on the commit hash of the released version you're using? Or would you recommend always trying to use code at the master hash and stay as up to date as possible? Either way, let's put a warning about this somewhere in the documentation for people.

Potentially the solution is a documentation page separate from Github that allows you to select your version, but that requires work...

And if I'm being honest, I don't think that you're "fully done" making a change to the library until all example throughout the codebase (including on codesandbox.io) reflect the new style. I don't think it's kosher to leave past examples up.

What would mitigate leaving past examples up is is if the version number (or commit hash) would be in import definitions at the top of the file, instead of in the package.json, such as import { elements, runComponent } from "@funkia/[email protected]"; However, I imagine that's not something you can change...

Confused by all the output options

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.)

Errors when running examples

Steps to reproduce:

git clone https://github.com/funkia/turbine/
cd turbine
yarn
cd examples/counters
yarn
npm run start

Output:

> [email protected] start /Users/dmitrizaitsev/Repos/turbine/examples/counters
> webpack-dev-server

Project is running at http://localhost:8080/
webpack output is served from /
ts-loader: Using [email protected] and /Users/dmitrizaitsev/Repos/turbine/examples/counters/tsconfig.json
Hash: 359266e80baf3486ab11
Version: webpack 2.5.1
Time: 7789ms
    Asset    Size  Chunks                    Chunk Names
bundle.js  869 kB       0  [emitted]  [big]  main
chunk    {0} bundle.js (main) 768 kB [entry] [rendered]
    [7] (webpack)/buildin/global.js 509 bytes {0} [built]
   [41] ./~/@funkia/jabz/dist/es/index.js 389 bytes {0} [built]
   [72] ./~/@funkia/hareactive/index.js 459 bytes {0} [built]
  [186] ./src/index.ts 1.64 kB {0} [built] [5 errors]
  [187] ./~/babel-polyfill/lib/index.js 833 bytes {0} [built]
  [188] (webpack)-dev-server/client?http://localhost:8080 5.68 kB {0} [built]
  [206] ./~/core-js/fn/regexp/escape.js 107 bytes {0} [built]
  [386] ./~/core-js/shim.js 7.38 kB {0} [built]
  [398] ./~/regenerator-runtime/runtime.js 24.4 kB {0} [built]
  [426] ./~/strip-ansi/index.js 161 bytes {0} [built]
  [427] ./~/url/url.js 23.3 kB {0} [built]
  [429] (webpack)-dev-server/client/overlay.js 3.73 kB {0} [built]
  [430] (webpack)-dev-server/client/socket.js 897 bytes {0} [built]
  [432] (webpack)/hot/emitter.js 77 bytes {0} [built]
  [443] multi (webpack)-dev-server/client?http://localhost:8080 babel-polyfill ./src/index.ts 52 bytes {0} [built]
     + 429 hidden modules

ERROR in /Users/dmitrizaitsev/Repos/turbine/src/component.ts
(34,14): error TS1219: Experimental support for decorators is a feature that is subject to change in a future release. Set the 'experimentalDecorators' option to remove this warning.

ERROR in ./src/version4.ts
(44,3): error TS2322: Type 'Component<{} & OutputStream<{}> & BehaviorOutput<{}> & DefaultOutput>' is not assignable to type 'Component<CounterModelInput>'.
  Type '{} & OutputStream<{}> & BehaviorOutput<{}> & DefaultOutput' is not assignable to type 'CounterModelInput'.
    Property 'incrementClick' is missing in type '{} & OutputStream<{}> & BehaviorOutput<{}> & DefaultOutput'.

ERROR in ./src/version4.ts
(56,27): error TS2345: Argument of type '({incrementClick, decrementClick, deleteClick}: CounterModelInput, id: number) => IterableIterato...' is not assignable to parameter of type 'Model1<CounterModelInput, ReactivesObject, {}, number>'.
  Type 'IterableIterator<Now<Behavior<number>> | ({ count: any; } | { count: any; deleteS: Stream<number>...' is not assignable to type 'ModelReturn<ReactivesObject, {}>'.
    Type 'IterableIterator<Now<Behavior<number>> | ({ count: any; } | { count: any; deleteS: Stream<number>...' is not assignable to type 'Iterator<Now<any> | [ReactivesObject, {}]>'.
      Types of property 'next' are incompatible.
        Type '(value?: any) => IteratorResult<Now<Behavior<number>> | ({ count: any; } | { count: any; deleteS:...' is not assignable to type '(value?: any) => IteratorResult<Now<any> | [ReactivesObject, {}]>'.
          Type 'IteratorResult<Now<Behavior<number>> | ({ count: any; } | { count: any; deleteS: Stream<number>; ...' is not assignable to type 'IteratorResult<Now<any> | [ReactivesObject, {}]>'.
            Type 'Now<Behavior<number>> | ({ count: any; } | { count: any; deleteS: Stream<number>; })[]' is not assignable to type 'Now<any> | [ReactivesObject, {}]'.
              Type '({ count: any; } | { count: any; deleteS: Stream<number>; })[]' is not assignable to type 'Now<any> | [ReactivesObject, {}]'.
                Type '({ count: any; } | { count: any; deleteS: Stream<number>; })[]' is not assignable to type '[ReactivesObject, {}]'.
                  Property '0' is missing in type '({ count: any; } | { count: any; deleteS: Stream<number>; })[]'.

ERROR in ./src/version4.ts
(91,58): error TS2345: Argument of type 'Behavior<number[]>' is not assignable to parameter of type 'Behavior<number[]>'.

ERROR in ./src/version4.ts
(96,38): error TS2344: Type 'ToView' does not satisfy the constraint 'ReactivesObject'.
  Property 'counterIds' is incompatible with index signature.
    Type 'Behavior<number[]>' is not assignable to type 'Behavior<any> | Stream<any>'.
      Type 'Behavior<number[]>' is not assignable to type 'Stream<any>'.
        Property 'combine' is missing in type 'Behavior<number[]>'.

ERROR in ./src/version3.ts
(42,17): error TS2453: The type argument for type parameter 'V' cannot be inferred from the usage. Consider specifying the type arguments explicitly.
  Type argument candidate 'CounterModelInput' is not a valid type argument because it is not a supertype of candidate '{} & OutputStream<{}> & BehaviorOutput<{}> & DefaultOutput'.
    Property 'incrementClick' is missing in type '{} & OutputStream<{}> & BehaviorOutput<{}> & DefaultOutput'.

ERROR in ./src/version3.ts
(66,44): error TS2345: Argument of type 'Behavior<number[]>' is not assignable to parameter of type 'Behavior<number[]>'.
  Types of property 'child' are incompatible.
    Type 'Observer<any>' is not assignable to type 'Observer<any>'. Two different types with this name exist, but they are unrelated.

ERROR in ./src/version3.ts
(70,49): error TS2345: Argument of type '({sum, counterIds}: ViewInput) => Iterator<Component<any>>' is not assignable to parameter of type 'View1<ReactivesObject, ModelInput, {}>'.
  Types of parameters '__0' and 'm' are incompatible.
    Type 'ReactivesObject' is not assignable to type 'ViewInput'.
      Property 'counterIds' is missing in type 'ReactivesObject'.

ERROR in ./src/version2.ts
(34,17): error TS2453: The type argument for type parameter 'V' cannot be inferred from the usage. Consider specifying the type arguments explicitly.
  Type argument candidate 'CounterModelInput' is not a valid type argument because it is not a supertype of candidate '{} & OutputStream<{}> & BehaviorOutput<{}> & DefaultOutput'.
    Property 'incrementClick' is missing in type '{} & OutputStream<{}> & BehaviorOutput<{}> & DefaultOutput'.

ERROR in ./src/index.ts
(25,17): error TS2345: Argument of type '{ class: string; classToggle: { active: Behavior<boolean>; }; }' is not assignable to parameter of type 'InitialProperties'.
  Types of property 'classToggle' are incompatible.
    Type '{ active: Behavior<boolean>; }' is not assignable to type '{ [name: string]: boolean | Behavior<boolean>; }'.
      Property 'active' is incompatible with index signature.
        Type 'Behavior<boolean>' is not assignable to type 'boolean | Behavior<boolean>'.
          Type 'Behavior<boolean>' is not assignable to type 'Behavior<boolean>'. Two different types with this name exist, but they are unrelated.
            Types of property 'child' are incompatible.
              Type 'Observer<any>' is not assignable to type 'Observer<any>'. Two different types with this name exist, but they are unrelated.
                Property 'changeStateDown' is missing in type 'Observer<any>'.

ERROR in ./src/index.ts
(28,29): error TS7031: Binding element 'click' implicitly has an 'any' type.

ERROR in ./src/index.ts
(32,3): error TS2345: Argument of type '({selectVersion}: { selectVersion: any; }) => Now<{ selected: Behavior<string>; }[]>' is not assignable to parameter of type 'Model1<{}, ReactivesObject, {}, {}>'.
  Type 'Now<{ selected: Behavior<string>; }[]>' is not assignable to type 'ModelReturn<ReactivesObject, {}>'.
    Type 'Now<{ selected: Behavior<string>; }[]>' is not assignable to type 'Iterator<Now<any> | [ReactivesObject, {}]>'.
      Property 'next' is missing in type 'Now<{ selected: Behavior<string>; }[]>'.

ERROR in ./src/index.ts
(36,15): error TS7031: Binding element 'selected' implicitly has an 'any' type.

ERROR in ./src/index.ts
(43,16): error TS2345: Argument of type '() => IterableIterator<Component<Stream<"1" | "2" | "3" | "4">> | { selectVersion: any; }>' is not assignable to parameter of type 'Child<{}>'.
  Type '() => IterableIterator<Component<Stream<"1" | "2" | "3" | "4">> | { selectVersion: any; }>' is not assignable to type '() => Iterator<Component<any>>'.
    Type 'IterableIterator<Component<Stream<"1" | "2" | "3" | "4">> | { selectVersion: any; }>' is not assignable to type 'Iterator<Component<any>>'.
      Types of property 'next' are incompatible.
        Type '(value?: any) => IteratorResult<Component<Stream<"1" | "2" | "3" | "4">> | { selectVersion: any; }>' is not assignable to type '(value?: any) => IteratorResult<Component<any>>'.
          Type 'IteratorResult<Component<Stream<"1" | "2" | "3" | "4">> | { selectVersion: any; }>' is not assignable to type 'IteratorResult<Component<any>>'.
            Type 'Component<Stream<"1" | "2" | "3" | "4">> | { selectVersion: any; }' is not assignable to type 'Component<any>'.
              Type '{ selectVersion: any; }' is not assignable to type 'Component<any>'.
                Object literal may only specify known properties, and 'selectVersion' does not exist in type 'Component<any>'.
webpack: Failed to compile.

Polishing up the explanation

I have been looking through the introduction to the output and it is still looks less clear than I'd like it to be. It is really a unique thing of this library and can easily scare away people if not made as simple as possible, or even simpler :)

Unfortunately, React didn't help making its components less confusing. There are component classes or constructors and there are instances. That look exactly the same in JSX, but who uses JSX anyway? :)

We don't have the JSX problem here, but it might be still more clear to emphasise the difference:

const componentInstance = myComponent({foo: "bar", something: 12}),
const view = div([
  componentInstance,
  componentInstance,
]);

Each line is a declaration of a piece of the state.

Not sure I understand this one ;)
The state is a vague concept - is any variable part of the state?
Or only the arguments of the view?

HTML-elements

Are they special component instances? Like in React?

The above covers the input to the counter view. We now need to get output from it. All components in Turbine can produce output. Components are represented by a generic type Component. The A represents the output of the component.

I would like to demystify "produce output" here.
So the component instance is wrapping its output.
Is it possible to extract it directly? Or is it impure?

The output object given to the button functions tells them what output to produce.

button({ output: { incrementClick: "click" } }, "+"),

So does it mean that only this output is produced?
So the whole output object is stored as { incrementClick: clickStream }?

  const {inputValue: email} = yield input();

This one is also a bit tricky, would it be possible to break down?
What does the yield do here?
E.g. in comparison with something like that:

  const {inputValue: email} = yield {inputValue: `[email protected]`}

So these are some questions a reader might come up with,
I've thought to put them for the record. :)

Simple server side rendering

With a way to stringify a component it would be possible to support simple server side rendering.

The API could consist of componentToString and serveComponent. The first would turn a component into a string and the second would return a NodeJS compatible endpoint function.

Example.

app.get("/app", serveComponent("index.html", "body", myComponent));

This would offer a really simple way of getting many of the benefits from server-side rendering with very little effort from end-users.

How to declare attributes

This issue is meant to be a continuation of the discussion about attributes that began in #48.

The problem is that when creating an element with attributes you currently have to give them in an attrs property.

input({attrs: {placeholder: "Foo"}});

This makes declaring attributes slightly more verbose than if they could be given directly on the object. This choice was made to keep attributes separate from all the other things that one can specify on the object to an HTML element function.

This PR is meant to explore if there is a way to make attributes less verbose without sacrificing other qualities.

not assignable to type errors

$ npm run counters

> [email protected] counters /Users/zaitsev/Repos/turbine/examples
> webpack-dev-server --content-base counters counters/src/index.ts

Project is running at http://localhost:8080/
webpack output is served from /
Content not from webpack is served from /Users/zaitsev/Repos/turbine/examples/counters
ts-loader: Using [email protected] and /Users/zaitsev/Repos/turbine/examples/counters/tsconfig.json
webpack: wait until bundle finished: /
Hash: 68ba1eafae3e545fd4ee
Version: webpack 2.2.1
Time: 9731ms
    Asset    Size  Chunks                    Chunk Names
bundle.js  681 kB       0  [emitted]  [big]  main
chunk    {0} bundle.js (main) 556 kB [entry] [rendered]
   [12] ./~/@funkia/jabz/dist/es/index.js 389 bytes {0} [built]
   [20] ../src/index.ts 401 bytes {0} [built]
   [29] ./~/@funkia/hareactive/index.js 569 bytes {0} [built]
   [88] ./counters/src/index.ts 1.64 kB {0} [built] [3 errors]
   [89] (webpack)-dev-server/client?http://localhost:8080 5.28 kB {0} [built]
  [102] ./counters/src/version1.ts 340 bytes {0} [built]
  [103] ./counters/src/version2.ts 905 bytes {0} [built] [1 error]
  [104] ./counters/src/version3.ts 1.66 kB {0} [built] [3 errors]
  [105] ./counters/src/version4.ts 2.26 kB {0} [built] [5 errors]
  [145] ./~/strip-ansi/index.js 161 bytes {0} [built]
  [146] ./~/url/url.js 23.3 kB {0} [built]
  [148] (webpack)-dev-server/client/overlay.js 3.6 kB {0} [built]
  [149] (webpack)-dev-server/client/socket.js 856 bytes {0} [built]
  [151] (webpack)/hot/emitter.js 77 bytes {0} [built]
  [171] multi (webpack)-dev-server/client?http://localhost:8080 ./counters/src/index.ts 40 bytes {0} [built]
     + 157 hidden modules

ERROR in ./counters/src/version4.ts
(43,3): error TS2322: Type 'Component<{} & OutputStream<{}> & BehaviorOutput<{}> & DefaultOutput>' is not assignable to type 'Component<CounterModelInput>'.
  Type '{} & OutputStream<{}> & BehaviorOutput<{}> & DefaultOutput' is not assignable to type 'CounterModelInput'.
    Property 'incrementClick' is missing in type '{} & OutputStream<{}> & BehaviorOutput<{}> & DefaultOutput'.

ERROR in ./counters/src/version4.ts
(55,41): error TS2345: Argument of type '({count}: CounterModelOut) => Component<CounterModelInput>' is not assignable to parameter of type 'View1<ReactivesObject, CounterModelInput, number>'.
  Type '({count}: CounterModelOut) => Component<CounterModelInput>' is not assignable to type '(m: ReactivesObject, a: number) => Iterator<Component<any>>'.
    Types of parameters '__0' and 'm' are incompatible.
      Type 'ReactivesObject' is not assignable to type 'CounterModelOut'.
        Property 'count' is missing in type 'ReactivesObject'.

ERROR in ./counters/src/version4.ts
(68,40): error TS2346: Supplied parameters do not match any signature of call target.

ERROR in ./counters/src/version4.ts
(90,58): error TS2345: Argument of type 'Behavior<number[]>' is not assignable to parameter of type 'Behavior<number[]>'.

ERROR in ./counters/src/version4.ts
(94,38): error TS2344: Type 'ToView' does not satisfy the constraint 'ReactivesObject'.
  Property 'counterIds' is incompatible with index signature.
    Type 'Behavior<number[]>' is not assignable to type 'Behavior<any> | Stream<any>'.
      Type 'Behavior<number[]>' is not assignable to type 'Stream<any>'.
        Property 'combine' is missing in type 'Behavior<number[]>'.

ERROR in ./counters/src/version3.ts
(42,17): error TS2453: The type argument for type parameter 'V' cannot be inferred from the usage. Consider specifying the type arguments explicitly.
  Type argument candidate 'CounterModelInput' is not a valid type argument because it is not a supertype of candidate '{} & OutputStream<{}> & BehaviorOutput<{}> & DefaultOutput'.
    Property 'incrementClick' is missing in type '{} & OutputStream<{}> & BehaviorOutput<{}> & DefaultOutput'.

ERROR in ./counters/src/version3.ts
(66,44): error TS2345: Argument of type 'Behavior<number[]>' is not assignable to parameter of type 'Behavior<number[]>'.
  Types of property 'child' are incompatible.
    Type 'Observer<any>' is not assignable to type 'Observer<any>'. Two different types with this name exist, but they are unrelated.

ERROR in ./counters/src/version3.ts
(70,49): error TS2345: Argument of type '({sum, counterIds}: ViewInput) => Iterator<Component<any>>' is not assignable to parameter of type 'View1<ReactivesObject, ModelInput, {}>'.
  Type '({sum, counterIds}: ViewInput) => Iterator<Component<any>>' is not assignable to type '(m: ReactivesObject, a: {}) => Iterator<Component<any>>'.
    Types of parameters '__0' and 'm' are incompatible.
      Type 'ReactivesObject' is not assignable to type 'ViewInput'.
        Property 'counterIds' is missing in type 'ReactivesObject'.

ERROR in ./counters/src/version2.ts
(34,17): error TS2453: The type argument for type parameter 'V' cannot be inferred from the usage. Consider specifying the type arguments explicitly.
  Type argument candidate 'CounterModelInput' is not a valid type argument because it is not a supertype of candidate '{} & OutputStream<{}> & BehaviorOutput<{}> & DefaultOutput'.
    Property 'incrementClick' is missing in type '{} & OutputStream<{}> & BehaviorOutput<{}> & DefaultOutput'.

ERROR in ./counters/src/index.ts
(25,17): error TS2345: Argument of type '{ class: string; classToggle: { active: Behavior<boolean>; }; }' is not assignable to parameter of type 'InitialProperties'.
  Types of property 'classToggle' are incompatible.
    Type '{ active: Behavior<boolean>; }' is not assignable to type '{ [name: string]: boolean | Behavior<boolean>; }'.
      Property 'active' is incompatible with index signature.
        Type 'Behavior<boolean>' is not assignable to type 'boolean | Behavior<boolean>'.
          Type 'Behavior<boolean>' is not assignable to type 'Behavior<boolean>'. Two different types with this name exist, but they are unrelated.
            Types of property 'child' are incompatible.
              Type 'Observer<any>' is not assignable to type 'Observer<any>'. Two different types with this name exist, but they are unrelated.
                Types of property 'changeStateDown' are incompatible.
                  Type '(state: State) => void' is not assignable to type '(state: State) => void'. Two different types with this name exist, but they are unrelated.
                    Types of parameters 'state' and 'state' are incompatible.
                      Type 'State' is not assignable to type 'State'. Two different types with this name exist, but they are unrelated.

ERROR in ./counters/src/index.ts
(39,35): error TS2344: Type 'FromModel' does not satisfy the constraint 'ReactivesObject'.
  Property 'selected' is incompatible with index signature.
    Type 'Behavior<"1" | "2" | "3" | "4">' is not assignable to type 'Behavior<any> | Stream<any>'.
      Type 'Behavior<"1" | "2" | "3" | "4">' is not assignable to type 'Stream<any>'.
        Property 'combine' is missing in type 'Behavior<"1" | "2" | "3" | "4">'.

ERROR in ./counters/src/index.ts
(51,31): error TS2346: Supplied parameters do not match any signature of call target.
webpack: Failed to compile.

Separating models from views

This issue is intended to be a follow-up to the discussion about the model-view separation that @jayrbolton started in #28.

One point I want to make is that I think achieving proper separation between model and view requires that the connection between the model and the view is sufficiently abstract.

Consider the following component below.

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

const counterView = ({ count }: CounterViewInput) => div([
  "Counter ",
  count,
  button({ class: "btn btn-default", output: { incrementClick: "click" } }, " + "),
  button({ class: "btn btn-default", output: { decrementClick: "click" } }, " - ")
]);

const counter = modelView(counterModel, counterView);

This is a simple counter component. The view contains an increment button and decrement button.

Let's say I want to wire up counterModel with a new view that has four buttons. Two buttons for increasing and decreasing the counter by one and two new buttons for changing the counter up and down by 5.

I can't really do that with the above model because the model has direct knowledge about the two buttons in the view. But, I can refactor the code to eliminate this:

function* counterModel({ delta }) {
  const count = yield sample(scan((n, m) => n + m, 0, delta));
  return [{ count }, { count }];
}

const counterView = ({ count }: CounterViewInput) => div([
  "Counter ",
  count,  button({ class: "btn btn-default", output: { incrementClick: "click" } }, " + "),
  button({ class: "btn btn-default", output: { decrementClick: "click" } }, " - ")
]).map(({ decrementClick, incrementClick }) => {
  return { delta: combine(incrementClick.mapTo(1), decrementClick.mapTo(-1)) });
});

const counter = modelView(counterModel, counterView);

What I've done is move the processing of the click events into the view. The model no longer has any knowledge of the detail that the view consists of two buttons. The model can be used with any view that outputs { delta: Stream<number> }.

With this change, I can easily reuse the exact same model and hook it up to the beforementioned view with 4 buttons.

const counterView = ({ count }: CounterViewInput) => div([
  "Counter ",
  count,
  button({ class: "btn btn-default", output: { increment5: "click" } }, "+5"),
  button({ class: "btn btn-default", output: { increment1: "click" } }, "+1"),
  button({ class: "btn btn-default", output: { decrement1: "click" } }, "-1")
  button({ class: "btn btn-default", output: { decrement5: "click" } }, "-5")
]).map(({ decrement1, decrement5, increment1, increment5 }) => {
  return {
    delta: combine(increment1.mapTo(1), increment5.mapTo(5), decrement1.mapTo(-1), decrement5.mapTo(-5))
  };
});

My point is: If a view does a minimal amount of preprocessing to its output then we can achieve a very loose coupling between model and view. The model expects input of a certain type. If that type contains as little knowledge about the view as possible the model will be highly reusable.

Fancy element creator

This issue is for writing down ideas related to a fancy way of creating elements based on tagged template literals.

Basic selector for adding classes and id.

li`#my-btn.btn.btn-default`(child)

Setting a class and a property with a string valued behavior.

input`.foo.${beh}.bar[value=${valB}]`(child)

Switching between classes with a boolean valued behavior and a syntax similair to JavaScript's a ? b : c.

li`.btn.${beh}?btn-default:btn-warning`(child)

The :-part is optional. So if only a single class is to be toggled on and off the following syntax can be used.

li`.btn.${beh}?btn-default`(child)

Just some ideas ๐Ÿ˜„

Remove `of`

Jabz no longer requires the returned value in go-notation to be a monad. We should update Jabz and get rid of the now superfluous ofs.

idea: JSX to Turbine

It'd be neat if we could write JSX in the Turbine views. It'd require a custom Turbine transform, as the output from today's JSX transformers output to a format designed for VDom.

Check out Surplus. It has it's own JSX transpiler, but the output is completely Surplus-specific.

Maybe Turbine could get something like this eventually.

Dispose listeners

Remove listeners when components are removed from DOM to avoid memory leaks.

Uncomponentisation and Universalisation?

(It is perhaps by now clear that)
I find this library really interesting and inspiring.

I have never seen the use of generators in this fashion,
if just one new idea to be mentioned, among others.

I really would like to leverage the unproject
to make it even more accessible and pluggable.

So anyone can try the Turbine with a simple piece of code,
and then instantly plug and use inside any existing working application.

Uncomponentisation

Specifically, what I mean by "uncomponent" here is to enable this example

const main = go(function*() {
  yield span("Please enter an email address: ");
  const {inputValue: email} = yield input();
  const isValid = map(isValidEmail, email);
  yield div([
    "The address is ", map((b) => b ? "valid" : "invalid", isValid)
  ]);
});

to be written as pure generator with no external dependency:

const view = ({ span, input, div }) => function*() {
  yield span("Please enter an email address: ");
  const {inputValue: email} = yield input();
  const isValid = map(isValidEmail, email);
  yield div([
    "The address is ", map((b) => b ? "valid" : "invalid", isValid)
  ]);
}

It is almost the same but now can be used and tested anywhere in its purity.
It would be recognised and packaged with go internally by the Turbine
at the runtime.

Universality

What I mean by universality is to be able to plug this into any application.
Here is how the un tries to make it work:

// the only place to import from un
const createMount = require('un.js')

// configure the mount function only once in your code
const mount = createMount({ 
 
  // your favorite stream factory 
  // mithril/stream, flyd, most, or hareactive?
  createStream: require(...),
 
  // your favorite element creator 
  // mitrhil, (React|Preact|Inferno).createElement, snabbdom/h, hyperscript, 
  //  or turbine.createElement?
  createElement: require(...),
 
  // convenience helpers for your favorite 
  createTags: require(...),
 // (possibly to be renamed in createElements)
 
  // mithril.render, (React|Preact|Inferno).render, snabbdom-patch 
  //  or turbine.render?
  createRender: require(...)
})

Use the mount function to run a Turbine's component directly in DOM:

const { actions, states } = mount({ el, model, view, initState: 0 })

or embed it anywhere into any framework:

const { actions, states } = MyComponent = mount({ model, view, initState: 0 })
    ...
// inside your React Component or wherever
    ...
render = () => 
    ...
    React.createElement(MyComponent, props)
    ...

// or with JSX if that's your thing:
    ...
    <MyComponent prop1={...}  prop2={...}  prop3={...}/>
    ...

Here is an example of the working code

Here is what I think is needed to make it possible:

  • Turbine's createElement function to compile the user uncomponents into Turbine components
  • Turbine's createRender function to run the component

It would be great to hear what you think about it.

User friendliness and other considerations

As I mentioned in this thread, I have a lot of time right now to dedicate to building out components for a frontend library/framework. I was going to change the name of flimflam to Uzu, add some improvements, and reorganize the way some of the modules are set up. However, I'd much rather join forces with another project.

I have some major hangups with Turbine, not with the concepts, but more around accessibility. For example, I would never feel comfortable introducing Turbine to the Oakland.js meetup group, because its heavy dependency on Typescript for examples, typescript signatures, and haskell-style functional abstractions would alienate the Node.js community out here.

However, I think it would be possible to basically "lift" Turbine above these more esoteric aspects. That is, what if there was a "layer" for this library's documentation that used only plain JS, no typescript, no typescript signatures, and common JS?

  • We could hide the dependency on generators
  • Make the examples basically look like flimflam, with stronger view-model separation, sort of like your celsius-fahren example that you gave me here: https://gist.github.com/jayrbolton/b38dfe8f2d8830dc01da0824f8598be1
  • We could hide or remove the peer dependency on Jabz.
  • We could hide the typescript and make everything es5 with only const and arrows (similar to the Node.js core documentation)

So this would be the "public layer" --- the topmost, immediate documentation that allows any common javascript developer to make an application.

Then, when the developer decides that they want to dig into the functional abstractions more, such as Jabz and the semantics of Hareactive, then they could view "deeper" documentation, perhaps in a wiki, that might show Typescript signatures and Haskell type classes, and so on. But these things would be under an optional "advanced" layer of documentation.

Strongly separating models from views

(This is more of an architectural consideration rather than accessibility)

In my recent experience building large-scale apps using flyd and snabbdom, we have come across one key principle that always helps in the maintainability of the app: strongly decouple Models with Views. The data tree of your markup should not try to match the data tree of your state and ui logic.

Models should even live in their own files and have no awareness of the views. Views should read from models and generate actions for them, but not much else. On examination, I think this is all very possible with Turbine. It would be nice if the documentation encouraged this style.

I actually have a real-world example of where this is important. On a single-page app, we have two sections: one section (section A) is for creating, updating, listing, and removing users, while the other section (section B) is for listing users, and for creating/updating/listing/removing nested user_roles. Both sections use different nested forms for all updates. Creating a user from section A should cause the user listing on section B to update. Likewise, creating a user_role in section B should cause the user listing in section A to update.

Most developers, using a "component" style, will have an instinct to create two separate components for section A and section B, each with their own separate models, stream logic, etc. However, they will have a terrible time trying to communicate the users back and forth between the two sections, and will find themselves with numerous cyclic dependencies, or resorting to mutating globals.

However, if that developer instead created a simple User model, and a UserRole model, which both used FRP to create/update/delete/list the users and user roles, without any markup, then they could simply pass instances of those models down through the views. The developer would never have to worry about redundant data between two siblings, cyclic data, redundant ajax logic, etc. The UI logic then becomes very easy.

The key here is that developers often have the instinct to map their data tree to their markup tree,. Say they had an html tree that generally looked like this:

markup:
  sectionA div
    table for users
    button for adding a user
    form for creating a user
  sectionB div
    table for users
    nested tables for user roles
    button for adding a role
    form for the role

Most developers would then have an instinct to create UI data and models with this structure:

data:
  sectionA
    isOpen: true/false
    users: array of user objects
    loading: true/false
    etc
  sectionB
    isOpen: true/false
    users: array of user objects
    user_roles: array of role objects
    loading: true/false
    etc

You can see how we are going to get cyclic dependencies between the sectionA model and sectionB model, because we want both sections to both read and update users

If the developer instead made their data model without thinking of the views, they would probably come up with:

data: 
  users:
    loading: true/false
    isEditing: true/false
    data: array of user objects
    user_roles:
      loading: true/false
      data: array of role objects
      etc

Here you can see that we no longer try to match the data tree to the markup tree, and we will eliminate cyclic dependencies.

Sorry if this is all too sloppily abstracted. Perhaps this real-world example would be a perfect practice test example for me to try with Turbine, so I can illustrate what I'm talking about with real code.

input value bug

This example expects the inputs to mirror each other. This behavior works in the beginning. You can pick either input box and change the value and it will mirror in the other. But if you modify the other, it won't change in the other one. And if you go back to the first one, it won't change the other anymore. Yet, when you put the behavior in another element (such as an span), it reflects that the stream is working correctly.

import { combine, sample, changes, stepper } from "@funkia/hareactive";
import { runComponent, elements, modelView, fgo, go } from "@funkia/turbine";
const { input, span } = elements;

const tempModel = fgo(function*({ val1_, val2_ }) {
  const v = yield sample(stepper(0, combine(changes(val1_), changes(val2_))));
  return { v };
});
const tempView = ({ v }) =>
  go(function*() {
    yield span(v);
    const { inputValue: val1_ } = yield input({ value: v });
    const { inputValue: val2_ } = yield input({ value: v });
    return { val1_, val2_ };
  });
const temp = modelView(tempModel, tempView);

runComponent("#app", temp());

https://codesandbox.io/s/rrwlvx41pq

Return value is not chained?

I was expecting both elements to show up but only first one did:

function* main() {
  yield label('Please enter your name:')
  return input()
}

It does show up when I remove the yield:

function* main() {
  return input()
}

A bug or feature? :)

Issues with lift

(I made this issue in Turbine because that's where I'm trying to use lift but I can move this to jabz if you prefer...)

  1. Do I import it from jabz and do lift(f, b1, b2)? Or I use it as b1.lift(f, b2)? It seems like most jabz things are importable from hareactive but not lift. Why is that?

  2. lift doesn't seem to work for 4 behaviors but the documentation says "You can also combine in this fashion any number of behaviors"

  3. lift doesn't work with loop parameters in the second argument spot with error this.fn.last is not a function:

https://codesandbox.io/s/p56qm7lkwj

const timer = loop(
  ({ x }) =>
    function*() {
      const x_ = Behavior.of(1);
      // works
      yield div(lift((a, b) => a + " " + b, Behavior.of(3), x));

      // does not work
      yield div(lift((a,b) => a + " " + b, x, Behavior.of(3)));

      return { x: x_ };
    }
);

Lighter view creation code

I'm slightly bothered by how the code for views looks in Funnel. And I've been thinking about how to make it more "lightweight", more easy on the eyes.

Consider this code this from the continuous time example:

function* view({time, message}: ToView): Iterator<Component<any>> {
  yield h1("Continuous time example");
  yield p(map(formatTime, time));
  const {click: snapClick} = yield p(button("Click to snap time"));
  yield p(message);
  return {snapClick};
}

And compare it to what creating a virtual dom for the same thing might look like:

function view({time, message}) {
  return [
    h1("Continuous time example"),
    p(formatTime(time)),
    p(button("Click to snap time")),
    p(message)
  ];
}

To me the later is easier to read. The long keywords like yield and const adds some distracting noise I think. Haskell's do-notation would have been much nicer ๐Ÿ˜ข

But besides extracting the snapTime stream the above view doesn't contain any logic. So maybe go-notation is overkill for simple cases like that.

When the above code does this:

const {click: snapClick} = yield p(button("Click to snap time"));

button returns a dictionary of all the streams and behaviors it produces. Destructuring is then used to take the stream with key click and get it out as a variable called snapClick. What if this renaming could take place inside button by giving it an object like this:

const {snapClick} = yield p(button({rename: {click: "snapClick"}, "Click to snap time"));

That looks worse than before. But by adding another new feature the view code could end up looking like this:

function view2({time, message}: ToView): Child {
  return [
    h1("Continuous time example"),
    p(map(formatTime, time)),
    p(button({rename: {click: "snapClick"}}, "Click to snap time")),
    p(message)
  ]
}

So, view2 returns a list of children that all returns an object. When such a list is used as a child our toComponent function should combine all the components (which it currently does) and also merge all the objects they return. Then the final returned object would contain a stream with the value snapClick as desired.

I not sure rename is a particularly good name for the property. But it would make creating simple views without any logic besides "destructuring-renaming" much more lightweight.

We might even build some sort of syntax for it into the fancy elements creator:

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.