ClojureScript, Om and Core.async: How to handle events properly
Asked Answered
D

1

20

I have had a look at using Om for rich client website design. This also is my first time using core.async. Reading the tutorial https://github.com/swannodette/om/wiki/Basic-Tutorial I have seen the usage of a core.async channel to handle the delete operation (as opposed to doing all the work in the handler). I was under the impression that using that channel for deletion was merely done because the delete callback was declared in a scope where you have a cursor on an item-level where you actually want to manipulate the list containing that item.

To get more insights into channels I have seen Rich Hickey's talk http://www.infoq.com/presentations/clojure-core-async where he explains how its a good idea to use channels to get application logic out of event-callbacks. This made me wonder whether the actual purpose of the delete channel in the tutorial was to show that way of structuring an application. If so,

  • what are best practices associated with that pattern?

  • Should one create individual channels for all kinds of events? I.e. If I add a controller to create a new event, would I also create a new channel for object creations that is then used to get objects to be added to the global state at another place in the application?

  • Lets say I have a list of items, and one items has a detailed/concise state flag. If detailed? is true it will display more information, if detailed? is false it will display fewer information. I have associated a on-click event that uses om/transact! on the cursor (being a view to the list item within the global state object).

(let [toggle-detail-handler 
      (fn [e]
        (om/transact! (get-in myitem [:state])
                      #(conj % {:detailed? (not (:detailed? %))})))]
  (html [:li {:on-click toggle-detail-handler}
         "..." ])) 

I realize that this might be a very succinct snippet where the overall benefit of using channels as a means to decouple the callback event from the acutal logic changes does at first not seem worth the effort but the overall benefits with more complex examples outweigh this. But on the other hand introducing an extra channel for such detail-not-detailed toggling seems to add a fair amount of load to the source code as well.

It would be great if you could give some hints/tips or other thoughts on the whole design issue and put them into perspective. I feel a little lost there.

Degraded answered 28/5, 2014 at 6:45 Comment(0)
C
16

I use channels to communicate between components that cannot communicate through cursors.

For example, I use channels when:

  • the communicating components do not share app state (eg, their cursors are pointing down different branches of a hierarchical data structure)
  • the changes being communicated live outside of the app state (for example, component A wants to change component B's local state AND component B is not a child of A (otherwise this can be done by passing :state to om/build)
  • I want to communicate with something outside of the Om component tree

Note that I like to keep the "domain state" in the app state atom and the GUI state in the component local state. That is, app state is what is being rendered and local state is how. (where "how" also refers to which part) For example, if you are writing a text editor, the app state is the document being edited and the local state is what page is being edited, whether bold is selected so forth.

In general, I use a single communication channel onto which I place [topic value] pairs. I then use pub and sub to route the messages. Eg, (def p (async/pub ch first)) to use the topic to dispatch events and (om/sub p my-ch :foo) to receive messages with topic :foo to my-ch. I generally store this single communication channel in Om's shared state.

Sometimes I will use multiple channels, but I would do this to set up specific pipelines or workflows, rather than for general purpose messaging. For example, if I have a pipeline of processing components doing stuff to a stream of data, then I might set this up as a chain of channels with the endpoints connected into my Om application. For general UI development, this is rare. I'm also playing around with a Qt-esque signals/slots system for my Om components and I'm still experimenting with using a shared signals channels vs having each signal be its own channel. I'm as yet undecided which approach is better.

Canonicate answered 10/6, 2014 at 14:23 Comment(2)
Interesting. I started with the same distinction between app state and local state, but now I consider local state information that I would throw away should the browser crash, everything else is app state to me. It became problematic when one equated app state with domain model. Like MS Word, if crashed and reopened, I'd expected the browser to remember current page and cursor position, so my app state would look like {:document [] :view { :page 1 :line 3 :char 4 } }, local state I might use for say Find dialog { :termSearched "blah" :match 4 :results 10 }.Japhetic
My view on this may change now in light of reference cursors in the latest Om. I will need to experiment more before I decide thoughCanonicate

© 2022 - 2024 — McMap. All rights reserved.