Fork me on GitHub
#hoplon
<
2020-04-27
>
jjttjj12:04:45

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

jjttjj12:04:50

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

jjttjj12:04:55

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

jjttjj12:04:05

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

alandipert20:04:49

@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

alandipert20:04:23

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

alandipert20:04:19

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

alandipert20:04:44

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

jjttjj20:04:45

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

alandipert20:04:46

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

alandipert20:04:13

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

jjttjj20:04:51

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

jjttjj20:04:44

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?

jjttjj20:04:07

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)

jjttjj20:04:31

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

alandipert21:04:49

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

alandipert21:04:04

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

jjttjj21:04:39

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

jjttjj21:04:00

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

phronmophobic21:04:59

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

jjttjj21:04:48

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

phronmophobic21:04:27

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

jjttjj21:04:44

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

jjttjj21:04:32

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

phronmophobic21:04:12

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

jjttjj21:04:41

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?

phronmophobic21:04:21

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

phronmophobic21:04:39

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

phronmophobic21:04:27

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

jjttjj21:04:50

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

phronmophobic21:04:42

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

phronmophobic21:04:44

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

jjttjj21:04:35

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

phronmophobic21:04:30

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

jjttjj21:04:14

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

phronmophobic21:04:41

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]

jjttjj21:04:54

I think I see it now

jjttjj21:04:11

thanks for the input!

phronmophobic21:04:38

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

jjttjj21:04:05

cool yeah I'll keep you posted in here