hoplon

The :hoplon: ClojureScript Web Framework - http://hoplon.io/
2020-04-27T12:41:45.118200Z

How do people feel about wrapping cells to become "mini magical objects" and have these be the basis of a ui app?

(defn file-nav [{:keys [cwd]}]
  (let [state (cell {:cwd cwd :files []})]
    (formulet [cwd (cell= (:cwd state))]
      (if cwd
        (ls cwd
          (fn [result]
            (swap! state assoc :files result)))
        (user-dir
          (fn [result]
            (swap! state assoc :cwd result)))))
    state))

2020-04-27T12:44:50.118900Z

Then using it like:

(let [fnav (file-nav {})
      new-path (cell nil)]
  (div
    (input :value new-path :change #(reset! new-path (.-target.value %)))
    (button :click #(swap! fnav assoc :cwd @new-path))
    (ul
      (for-tpl [{:keys [relative-path]} (cell= (:files fnav))]
        (li relative-path)))))

2020-04-27T12:48:55.120800Z

A bigger not quite coomplete example

(defn editor []
    (let [cm  (cm/->CodeMirror default-config)
          obj (cell {:text       ""
                     :dirty?     false
                     :generation 0
                     :cursor     {:line 0 :ch 0}
                     :renderable (cm/->elem cm)
                     :cm         cm
                     })

          txt       (cell= (:text obj))
          clean-gen (cell nil)]

      ;;when dirty changes to false, reset the generation
      (formulet [clean (cell= (not (:dirty? obj)))]
        (when clean
          (reset! clean-gen (cm/generation cm))))

      (cell=
        (when (not= txt (cm/text-value cm))
          (cm/set-val-and-keep-cursor cm txt)))

      (cm/on-change cm (fn [cm delta]
                         (swap! obj (fn [obj]
                                      (-> obj clog
                                        (assoc
                                          :text (cm/text-value cm)
                                          :generation (cm/generation cm)
                                          :dirty? (spy (cm/dirty? cm @clean-gen))))))))

      (cm/on-move cm (fn [cm]
                       (swap! obj assoc :cursor (cm/->cursor cm))))
      obj
      ))
Here's a CodeMirror instance which we manage with all these reactive cells. I guess the idea is to totally hide CodeMirror behind a cell and have these managed essentially by internal watchers

2020-04-27T12:52:05.123100Z

This works ok for me somewhat so far but I'm trying to figure out where it would break down and no longer be worth it, or what types of "objects" this won't be possible with. For one, it becomes somewhat awkward to have side effects that aren't associated with a piece of state. I'm also kind of thinking it's bad to have swap!/`reset!` be the one way to interface with an object, without any real idea of what the side effects can be

2020-04-27T20:31:49.124200Z

@jjttjj it reminds me of the original scheme approach to object orientation. the idea of a function that returns a closure and the closure serves as a "remote control" into the closure state

2020-04-27T20:32:23.124700Z

i think the affordances of a cell as the remote control is maybe a separate question from that of setting up cells internally to manage the lifetime of a stateful object

2020-04-27T20:34:19.125900Z

i like the hybrid approach you appear to have arrived at. cm is a cell of a map, but the key values come from cm/ constructor functions

2020-04-27T20:34:44.126300Z

at least one interesting property of that is your updates to the wrapped object are atomic

2020-04-27T20:46:45.128800Z

@alandipert thanks for the input! I've been trying to work with this type of setup for a day or two now. Not quite sure what I think yet. That's a good point about the cell as a remote being separate from the cell as the manager of the state.

2020-04-27T20:46:46.129Z

otoh, if you were using named "mutator" functions you could conceivably wrap them in a transaction

2020-04-27T20:47:13.129800Z

one of the classic pitfalls of OOP and REST is the inability to perform mutations atomically

2020-04-27T20:47:51.130100Z

I think someone asked in here about the level of granularity cells should be at and now I'm kind of stuck on the same question

2020-04-27T20:48:44.131100Z

Do you mean like have the "mutator functions" operate on an immutable map and then put that map in a cell at some higher level?

2020-04-27T20:51:07.133600Z

I'm basically working on a little in-browser editor where I sort of steal windows+buffers setup from emacs. So there are buffers within windows, and buffers can be editors or something else (like a file navigator thing)

2020-04-27T20:52:31.134200Z

and going back and forth on how to setup what exactly should be cells

2020-04-27T21:04:49.135500Z

re: mutator functions i was imagining calling functions on some object instead of swap!-ing on a cell associated with the object

2020-04-27T21:05:04.135900Z

but under the hood maybe it's just swapping. the advantage of the functions being, you can document/import/export etc them

2020-04-27T21:05:39.136500Z

yeah true, I was thinking swap! isn't the best "universal interface"

2020-04-27T21:07:00.137500Z

but then at some point doing (set-thing this new-value) for a bunch of things didn't feel quite right either, but it might be better

phronmophobic 2020-04-27T21:08:59.139700Z

one thing re-frame and cljfx do is instead of directly doing set-thing, they allow you to either return or emit values. dispatch for re-frame and specifying a map as a hander in cljfx

2020-04-27T21:09:48.140900Z

I sort of have a feeling something like this https://github.com/domino-clj/domino might be what I'm looking for, where the model/events it can receive and effects that can happen are explicitly stated. but I've kind of struggled to really "get it" in a few quick attempts to actually use it, which is causing me to question the overall ergonomics of the library, but this might just be me

phronmophobic 2020-04-27T21:10:27.142Z

I tried a similar strategy as above (creating cells that represent ui elements), but I couldn’t quite figure out the best strategy

2020-04-27T21:10:44.142200Z

I've been thinking a little bit about the event driven dispatch apprach

2020-04-27T21:12:32.143700Z

It feels like there should be some means of keeping the reactive state local when it's only needed locally but it might be hard to have a uniform approach to this

phronmophobic 2020-04-27T21:15:12.146200Z

i’ve found that the state you want to stay local depends on the context. in production, you might only care about the editor’s text, but in development, you may care about the cursor, text selection, etc. for testing. the direction I’ve been trying to move towards is to not have the component decide which state should be local, but allow the consumer of the component decide. however, sane defaults should be provided (eg. defaulting text selection and cursor management to being “local state”).

2020-04-27T21:19:41.148500Z

That's interesting. By "let the consumer decide" do you mean basically just have a protocol that the consumer uses and the component implements? So that you could have a "bunch of cells" implementation as well as a "global dispatch" implementation?

phronmophobic 2020-04-27T21:25:21.154200Z

it’s two pieces: 1. All state are properties that you can pass in. using text editor as an example (text-editor :text text-cell) if you only care about the text or (text-editor :text text-cell :cursor cursor-cell) if you also care about the cursor 2. events don’t directly happen, so the text editor doesn’t directly modify the cursor, it just suggests that the cursor should be modified which the parent can intercept like:

(on-wrap :cursor-update 
   (fn [orig-handler new-cursor] 
     (if (even? new-cursor) 
       (orig-handler new-cursor) 
      [:cursor-update (inc new-cursor)]))
   (text-editor :text text-cell :cursor cursor-cell))

phronmophobic 2020-04-27T21:25:39.154600Z

this is pseudo-code, but hopefully conveys the basic idea

phronmophobic 2020-04-27T21:27:27.155200Z

if a property (eg. cursor) isn’t passed, then a cell with a default value is created and used

2020-04-27T21:30:50.157300Z

So on-wrap basically attaches a listener to the text-editor, which can emit :cursor-update events? What is orig-handler?

phronmophobic 2020-04-27T21:32:42.158200Z

oh whoops. I was using the wrong example. it should just be:

(on :cursor-update 
   (fn [new-cursor] 
     (if (even? new-cursor) 
      [:cursor-update new-cursor]
      [:cursor-update (inc new-cursor)]))
   (text-editor :text text-cell :cursor cursor-cell))

phronmophobic 2020-04-27T21:34:44.160200Z

on can be used for bubbling (ie. child elements emitting responses to events). on-wrap is a different thing if you want to catch incoming events and modify them for child elements:

;; text-editor can handle all key presses except enter
;; when enter is pressed, subtmit a form instead
(on-wrap :key-press 
   (fn [orig-handler key] 
     (if (= key :enter)
      [:submit-form]
      (orig-handler key))
   (text-editor :text text-cell :cursor cursor-cell))

2020-04-27T21:36:35.161600Z

Gotcha, I guess I'm just still confused about:

(on :cursor-update ;;I presume this is an event type the text-editor emits? Or is this just watching cursor-cell
   (fn [new-cursor] 
     (if (even? new-cursor) 
      [:cursor-update new-cursor] ;;what happens with this return value? 
      [:cursor-update (inc new-cursor)]))
   (text-editor :text text-cell :cursor cursor-cell))

phronmophobic 2020-04-27T21:38:30.163400Z

yes, :cursor-update is an event you expect the text editor to emit

;; somewhere in text-editor
(on :key-press (fn [key] 
                  [ [:change-text text-cell key] 
                    [:update-cursor (inc cursor))] 
                   ])
    (render-text-view text cursor))

2020-04-27T21:40:14.164400Z

and [:change-text text-cell key] would be sent back to the render-text-view to actually make the mutation to the [javascript] text editor oop thing

phronmophobic 2020-04-27T21:41:41.165900Z

ideally, all the information needed to process the event is included in the event itself. if the event needs more information, then you should include it. [:change-text text-cell key oop-thing]

2020-04-27T21:42:54.166500Z

I think I see it now

2020-04-27T21:43:11.166800Z

thanks for the input!

phronmophobic 2020-04-27T21:45:38.167600Z

these ideas are still a WIP, so any feedback and improvements are helpful

2020-04-27T21:48:05.168100Z

cool yeah I'll keep you posted in here