Skip to content
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

snabbdom tutorial #14

Closed
yelouafi opened this issue Jul 28, 2015 · 12 comments
Closed

snabbdom tutorial #14

yelouafi opened this issue Jul 28, 2015 · 12 comments

Comments

@yelouafi
Copy link
Contributor

@paldepind I just finished writing a post about developing virtual-DOM applications using snabbdom + Elm architecture. the focus is on writing application using pure functions

the url is here
https://medium.com/@yelouafi/react-less-virtual-dom-with-snabbdom-functions-everywhere-53b672cb2fe3

the article is not yet published.i'm interested in any comments regarding adding of modifying something.

@paldepind
Copy link
Member

This is very interesting! I don't have time for a throughout read right now :(

But I really like your recursive main function. It very clearly and simply expresses the cyclic nature without relying on FRP (which the Elm architecture to be honest doesn't really depend on). I was however a bit confused by the handler function. Probably mostly due to it's name. What about calling it update like the other update functions?

@yelouafi
Copy link
Contributor Author

Thanks for your notes! I think you"re right about the handler naming. I also probably should add the init method like in Elm (actually i use an init action).

@paldepind
Copy link
Member

Using object spread properties is a really nice way to modify object without mutating them. I didn't knew such a thing existed.

I'm still very impressed by the elegance of the asynchronously recursive main function. That thing is a real beauty! It reminds me of the asynchronous recursion in your blog post on promise based FRP. It also reminds me of the temporal recursion over event in the Fran FRP paper. I'm going implement at least one of the examples in noname with that concept.

I really like the blog post. I think the examples and explanations are splendid. If I were you I'd go over the post one more time for grammar. I can also add more Medium notes if it is ok with you.

Why have you chosen not to use init functions? I think having them is quite nice. They serve as documentation for the model (you achieve that with comments though). For more complex models they can do validation and/or initialize dependent data (for instance if we have a model with startTime and duration the init function can automatically add a endTime property. They also makes it explicit in the code when we are creating instances of the model.

@ericgj
Copy link
Contributor

ericgj commented Jul 30, 2015

I agree, great work @yelouafi ! I like how you build up the concepts from a series of examples. I too am impressed with that main function. It means you don't have to introduce Elm Signals/flyd streams/etc. at all to show the benefits of the architecture. Which is important I think because people get very mystified by FRP (myself included).

My main suggestion would be to split it into chapters (if Medium lets you do that). For instance: I found it a bit hard to follow when you are in some cases adding to previous examples and in other cases starting 'from scratch'; sometimes there is an interlude when you're introducing new concepts, etc. but the breaks are not obvious in one long article. Chapters would let you group the examples in a logical way together with the concepts. Also you could split out the mechanical stuff into its own chapter (npm , browserify, watchify, etc.).

And you could add a longer bit about your TodoMVC. That 'simple router' is quite interesting and worth walking through. Also your tests which you touch on, but IMO it's at least worth pointing out just how nice it is to be able to test application state transitions independent of the DOM (independent even of the views) like this. 👍

@yelouafi
Copy link
Contributor Author

@paldepind thanks again for you notes! i appreciate you help. I'll recheck the post (English grammar isn't my strong point). Please feel free to add any relevant note you see.

My main hesitation on adding the 'init' function is that it can be expressed through an 'init' action. i felt that i'd be better keeping number of concepts to the minimum. But i'm not myself convinced 100%, one part of me keep wanting to add an explicit 'init'. Do you think adding an explicit function leads to better understanding than an 'init' action ?

@ericgj thank you! glad you liked the post. Medium is quite restrictive when it comes to organizing your content; there is only 3 title styles (h1 for the main title, and h2 and h3 for the rest). the article is split in 2 parts : a snabbdom tutorial and the elm architecture, inside the 2nd i followed the same path as in Elm tutorial. Do you see a better way? i'm open to any suggestion. As for the MVC i felt that the article was already too long. maybe i can add another post on it.

An issue i'd like to hear your opinion about is relative to external events. Actually the way 'main' is implemented makes it non trivial on how to integrate external events like triggering an async action or handling out-of-view events like window's events. For example in this example

function async(handler) {
  handler(Action.RequestStart());
  asyncRequest(..., response => handler(Action.RequestEnd() ); // won't necessarily work !
}

function view (model, handler) {
  h('button', on: { click: () => async(handler);
}

If meantime the view was invoked before the asyncRequest completes (i.e. a user event was triggered between the request's start and end). the handler parameter will become obsolete (the state captured in its closure inside main is no longer valid). actually, i've implemented 2 examples with 2 different solutions

the first uses a mutable variable 'currentHandler' that's updated in every view invocation: i don't like this solution but it has the advantage of simplicity

the second tries to isolate the currentHandler mutation inside a separate module. and triggers the mutation from the 'patch' function call by using the hooks feature of snabbdom. i used the same solution to connect to the window 'hashchange' events in the todoMVC example. there is still a mutation but it's kept separated from the component.

But perhaps there is a better or simpler solution. I'm not sure but i think in Elm async requests are handled by the runtime.

@paldepind
Copy link
Member

@yelouafi

My main hesitation on adding the 'init' function is that it can be expressed through an 'init' action. i felt that i'd be better keeping number of concepts to the minimum. But i'm not myself convinced 100%, one part of me keep wanting to add an explicit 'init'. Do you think adding an explicit function leads to better understanding than an 'init' action?

I can understand your idea. Getting rid of the init function is certainly simpler. But there is one thing I don't like about it. The update function takes a state and an action and produces a new state. The init function just takes arguments and creates a state.

To see what I mean take a look at this code from your tutorial:

function update(count, action) {
  return  Action.case({
    Increment : () => count + 1,
    Decrement : () => count - 1,
    Init      : number  => number
  }, action);
}

There is something very different about the init action and the other actions. The other actions consider the current state in what they return. The init action does not. The update function is for updating the state. The init function is for constructing a brand new state. I think mixing the initializing with the updating hides this difference.

@yelouafi
Copy link
Contributor Author

@paldepind i see, the 2 operations are different in their purposes, meanings and type signatures. that's more than sufficient to introduce in a separate function. I'll modify the post then.

@paldepind
Copy link
Member

@yelouafi

the first uses a mutable variable 'currentHandler' that's updated in every view invocation: i don't like this solution but it has the advantage of simplicity

I think there is something wrong with the example here. You pass the handler to getAsyncMsg. But you don't use it inside the function. You could of course only use the passed handler synchronously but not asynchronously since the handler would be outdated at that time. That is the real problem as far as I can see and why you need the mutation.

A similar but slightly different idea. What if we give up the nice sweet looking main function and turn it into something like this imperative thing:

let state, vnode, update, view
function main(initState, element, {_view, _update}) {
  state = initState
  vnode = element
  view = _view
  update = _update
  view(state, handler)
}
function handler(action) {
  state = update(state, action)
  vnode = patch(vnode, view(state, render))
}

The main difference here is that handler function is no longer an anonymous function that is "renewed" at each update. Instead it is constant. With that implementation the async function in your first example could be implemented like this:

function getAsyncMsg(handler) {
  handler({type: ASYNC_START});
  setTimeout(() => {
    handler({ type: ASYNC_END, data: 'Hello async'});
  }, 2000);
}

Because the handler is never outdated. I'm not sure if this is a better or worse solution but it does appear to be a solution. And it appears to be quite natural in usage even though the implementation of the main function turns out to be much less elegant.

@yelouafi
Copy link
Contributor Author

yelouafi commented Aug 2, 2015

I think your solution is the most practical one. The mutation inside 'main' avoids all the other dirty tricks inside components. But there can be an issue if the main handler was wrapped inside a dynamic handler before passed down to a nested component. although the wrapped/main handler is static. If the dynamic handler relies on some data on the model. there is a risk of using an outdated version of that data.

@paldepind
Copy link
Member

But there can be an issue if the main handler was wrapped inside a dynamic handler before passed down to a nested component. although the wrapped/main handler is static. If the dynamic handler relies on some data on the model. there is a risk of using an outdated version of that data.

Yes. That is true. I had not thought about that. I think it can be avoided and worked around in practice but it might still be a source of confusion.

@paldepind
Copy link
Member

Is this article done? I'd like to tweet it and include a link to it in the readme of the repository on noname repository.

@yelouafi
Copy link
Contributor Author

yelouafi commented Aug 5, 2015

Yes. Thanks for sharing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants