Fork me on GitHub
#clerk
<
2023-05-15
>
rickmoynihan14:05:02

I’d like to put a text area into a clerk notebook which dynamically evaluates the contents of the text area with sci and outputs the result to a pre tag. I’m sure I’d seen something that did this somewhere, but can’t seem to find it… any pointers?

Sam Ritchie14:05:15

Can you give an example of what you’d like to output?

borkdude14:05:30

where are the rendered notebooks/* from the clerk repo available? the cherry notebook has one such example, probably it's possible to use SCI as the evaluator too

👀 2
rickmoynihan14:05:51

I’d like an editable textarea with a sci form in it, e.g. (range) And I’d like that textarea to be evaluated on every change or on a submit button with preferably either the clerk data structure viewers displaying the result, or a pr-str in a pre tag. So in the above case the result would obviously be something like: (1 2 3 4 5 6 7 8 9 10 ...)

borkdude14:05:58

and then look for "dynamic"

borkdude14:05:24

no sorry, "Input text and compile on the fly with cherry"

rickmoynihan14:05:45

ahh awesome! Thanks 🙇

borkdude14:05:44

If you would rather use SCI for evaluation I think you could just use load-string on the contents of your text-area

rickmoynihan14:05:51

hmm I get error in render-fn: Could not resolve symbol: nextjournal.clerk.cherry-env/cherry-compile-string

rickmoynihan14:05:10

in the browser env

rickmoynihan14:05:28

ahh maybe missing a def

rickmoynihan14:05:00

hmm no doesn’t look like it

borkdude14:05:00

This is only available from clerk from the main branch I think... right @U5H74UNSF?

borkdude14:05:26

but as I said, you can also just use load-string on the code string in your text area via SCI

borkdude14:05:17

Hmm, I don't even see cherry mentioned in the changelogs...

borkdude14:05:41

(probably my bad)

rickmoynihan15:05:17

> but as I said, you can also just use load-string on the code string in your text area via SCI I’m trying to understand what this means exactly… I don’t really understand the clerk evaluation model; and how much magic there is

borkdude15:05:43

• the example from the cherry notebook should work if you are using the main version with git/sha • this should work if you are using the clojars version:

(clerk/with-viewer
  {:evaluator :sci
   :render-fn
   '(fn [value]
      (let [default-value "(defn foo [x] (+ x 10))
(foo 10)"
            !input (reagent.core/atom default-value)
            !val (load-string @!input)
            click-handler (fn []
                            (reset !val (try (load-string @!input))))]
        (fn [value]
          [:div
           [:div.flex
            [:div.viewer-code.flex-auto.w-80.mb-2 [nextjournal.clerk.render.code/editor !input]]
            [:button.flex-none.bg-slate-100.mb-2.pl-2.pr-2
             {:on-click click-handler}
             "Compile!"]]
           
           [nextjournal.clerk.render/inspect
            @!val]])))}
  nil)

borkdude15:05:59

Note that the above code might need some tweaks, I just edited by hand and probably made a mistake or two

jackrusher15:05:43

@U06HHF230 what’s your use case for this feature, btw?

rickmoynihan15:05:00

The use case isn’t anything major… Essentially I’m doing an intro to clojure talk, and I’m using clerk-slideshow, and would like my simple code evaluation examples to be editable in my slides, so I don’t need to faff about switching to my text editor etc

👍 2
rickmoynihan15:05:46

so I can live-code some examples in my slides if there are questions etc

rickmoynihan15:05:18

@U04V15CAJ: hmm your modified example gives me the same error :thinking_face:

Daniel Craig15:05:42

you could also use #C1VHLE27L

rickmoynihan15:05:17

Yeah using klipse had occurred to me too… but I was assuming that as clerk has most of the pieces it’d be a little less faffy to use what it provides, and would enable more consistent integration with the data viewers etc

rickmoynihan15:05:02

ahh maybe its a caching problem

rickmoynihan15:05:54

ok those problems sorted… but still not quite working :thinking_face:

borkdude15:05:38

What do you mean, same error, what error?

rickmoynihan15:05:59

that one’s sorted now

borkdude15:05:16

Can you please share your notebook on GitHub so we can talk more accurately? :)

rickmoynihan15:05:22

now I’m not getting an error, the first eval works, but clicking compile after making changes isn’t re-evaluating the form… currently debugging

borkdude15:05:43

hmm, it seems the atom isn't updated properly. This is what I have now:

(clerk/with-viewer
  {:evaluator :sci
   :render-fn
   '(fn [value]
      (let [default-value "(defn foo [x] (+ x 10))
(foo 10)"
            !input (reagent.core/atom default-value)
            !val (atom (load-string @!input))
            click-handler (fn []
                            (prn @!input)
                            (reset! !val (load-string @!input)))]
        (fn [value]
          [:div
           [:div.flex
            [:div.viewer-code.flex-auto.w-80.mb-2 [nextjournal.clerk.render.code/editor !input]]
            [:button.flex-none.bg-slate-100.mb-2.pl-2.pr-2
             {:on-click click-handler}
             "Eval!"]]
           [nextjournal.clerk.render/inspect
            @!val]])))}
  nil)

borkdude15:05:52

Ah gotcha, I needed to change atom to reagent.core/atom:

(clerk/with-viewer
  {:evaluator :sci
   :render-fn
   '(fn [value]
      (let [default-value "(defn foo [x] (+ x 10))
(foo 10)"
            !input (reagent.core/atom default-value)
            !val (reagent.core/atom (load-string @!input))
            click-handler (fn []
                            (prn @!input)
                            (reset! !val (load-string @!input)))]
        (fn [value]
          [:div
           [:div.flex
            [:div.viewer-code.flex-auto.w-80.mb-2 [nextjournal.clerk.render.code/editor !input]]
            [:button.flex-none.bg-slate-100.mb-2.pl-2.pr-2
             {:on-click click-handler}
             "Eval!"]]
           [nextjournal.clerk.render/inspect
            @!val]])))}
  nil)

borkdude15:05:17

And this is with better error handling:

(clerk/with-viewer
  {:evaluator :sci
   :render-fn
   '(fn [value]
      (let [default-value "(defn foo [x] (+ x 10))
(foo 10)"
            !input (reagent.core/atom default-value)
            safe-load-string (fn [s]
                               (try (load-string s)
                                    (catch :default e e)))
            !val (reagent.core/atom (safe-load-string @!input))
            click-handler (fn []
                            (reset! !val (safe-load-string @!input)))]
        (fn [value]
          [:div
           [:div.flex
            [:div.viewer-code.flex-auto.w-80.mb-2 [nextjournal.clerk.render.code/editor !input]]
            [:button.flex-none.bg-slate-100.mb-2.pl-2.pr-2
             {:on-click click-handler}
             "Eval!"]]
           [nextjournal.clerk.render/inspect
            @!val]])))}
  nil)

rickmoynihan15:05:42

hmm I just made that same change, but doesn’t seem to work… trying yours incase there’s something else up

rickmoynihan15:05:05

ah ok yours works

rickmoynihan15:05:45

Thanks that’s great! 🙇

👍 2
mkvlr15:05:06

try (nextjournal.clerk/show! "")

👀 2
rickmoynihan15:05:08

hmm so changing borkdude’s example to drop the eval button and use an :on-change handler on the editor doesn’t appear to work :thinking_face:

rickmoynihan15:05:25

(clerk/with-viewer
  {:evaluator :sci
   :render-fn
   '(fn [value]
      (let [default-value "(defn foo [x] (+ x 10))
(foo 30)"
            !input (reagent.core/atom default-value)
            !val (reagent.core/atom (load-string @!input))
            click-handler (fn []
                            (prn @!input (load-string @!input))
                            (reset! !val (load-string @!input)))]
        (fn [value]
          [:div
           [:div.flex
            [:div.viewer-code.flex-auto.w-80.mb-2 [nextjournal.clerk.render.code/editor
                                                   !input
                                                   {:on-change click-handler}]]
            ]
           [nextjournal.clerk.render/inspect
            @!val]])))}
  nil)

mkvlr15:05:47

this is simpler

(def editor-sync-viewer
  (assoc viewer/viewer-eval-viewer :render-fn '(fn [!code _]
                                                 [:div.bg-neutral-50 [nextjournal.clerk.render.code/editor !code]])))

mkvlr15:05:56

then use it via

^{::clerk/sync true ::clerk/viewer editor-sync-viewer}
(defonce editable-code (atom "(def fib
  (lazy-cat [0 1]
            (map + fib (rest fib))))"))

mkvlr15:05:30

I believe @U9EQP1K0X you have plenty of examples of eval with an editable code viewer, right?

rickmoynihan15:05:40

@U5H74UNSF: where do the evaluation results in that example get displayed?

rickmoynihan15:05:53

Or are you suggesting to combine it with the borkdude snippet?

mkvlr15:05:13

@U06HHF230 do you want to eval on change or on click?

rickmoynihan15:05:24

eval on change preferably

rickmoynihan15:05:55

if it gets too slow I can debounce it

rickmoynihan16:05:14

but the snippets I’m planning to use it with should be fine for on-change

mkvlr16:05:04

and where do you want to eval? JVM or SCI or doesn’t matter?

rickmoynihan16:05:43

SCI would be fine… but if it’s possible to get it to do in the JVM without too much hoop jumping that would also be ace

rickmoynihan16:05:12

I might do some interop examples… but I was mainly just hoping to do basic evaluation with these examples, so sci would be fine

mkvlr16:05:14

JVM with a sync atom is easiest imo

mkvlr16:05:44

so the two snippets above and a cell with (eval (read-string @editable-code)) should work (sorry just typing on 📱 )

rickmoynihan16:05:00

ok I think I see what the last example is actually doing now :thinking_face:

mkvlr16:05:31

a top level form, sorry

rickmoynihan16:05:22

what context does :render-fn in those snippets run in? I’m assuming they’re just SCI or CLJ but SCI/CLJS in the browser?

mkvlr16:05:39

render fns run in the browser through sci, sync atom state is synced to the JVM (and back)

borkdude16:05:02

eval + read-string = load-string

👍 2
rickmoynihan16:05:02

Does sci do meta-circular eval? i.e. from within the sci render function can you run sci’s eval-string?

borkdude16:05:02

yes, just use load-string

borkdude16:05:27

that's what my example already did

rickmoynihan16:05:44

yeah ok — playing with mkvlr’s one

rickmoynihan16:05:49

that seems to work

rickmoynihan16:05:35

;; # :necktie: Code Viewer
(ns viewers.code
  {:nextjournal.clerk/no-cache true}
  (:require [nextjournal.clerk :as clerk]
            [nextjournal.clerk.viewer :as viewer]
            ))

(def editor-sync-viewer
  (assoc viewer/viewer-eval-viewer :render-fn '(fn [!code _]
                                                 [:<>
                                                  [:div.bg-neutral-50 [nextjournal.clerk.render.code/editor !code]]
                                                  [:pre (load-string @!code)]])))


^{::clerk/sync true ::clerk/viewer editor-sync-viewer
  ::clerk/visibility {:code :hide}}
(def editable-code
  (atom "(defn add [& args] (apply + args))
(add 10 20)"))

mkvlr16:05:29

that is evaluating in sci

mkvlr16:05:33

I meant this

mkvlr16:05:00

;; # :necktie: Code Viewer
(ns viewers.code
  {:nextjournal.clerk/no-cache true}
  (:require [nextjournal.clerk :as clerk]
            [nextjournal.clerk.viewer :as viewer]
            ))

(def editor-sync-viewer
  (assoc viewer/viewer-eval-viewer :render-fn '(fn [!code _]
                                                 [:div.bg-neutral-50 [nextjournal.clerk.render.code/editor !code]])))


^{::clerk/sync true ::clerk/viewer editor-sync-viewer
  ::clerk/visibility {:code :hide}}
(def editable-code
  (atom "(defn add [& args] (apply + args))
(add 10 20)"))

(load-string @editable-code)

rickmoynihan16:05:31

I think I tried a variant like that but it didn’t seem to refresh the evaluated result 👀

rickmoynihan16:05:04

I guess to fix that you need to put the result in an atom too??

mkvlr16:05:25

no, looking into it

borkdude16:05:36

that sync thing seems to work, but it seems it re-evaluates all the cells of the entire nodebook on every character of input

👍 2
mkvlr16:05:48

this works, it was missing a defonce and I removed the no-cache:

(ns scratch
  (:require [nextjournal.clerk :as clerk]
            [nextjournal.clerk.viewer :as viewer]))

(def editor-sync-viewer
  (assoc viewer/viewer-eval-viewer :render-fn '(fn [!code _]
                                                 [:div.bg-neutral-50 [nextjournal.clerk.render.code/editor !code]])))


^{::clerk/sync true
  ::clerk/viewer editor-sync-viewer
  ::clerk/visibility {:code :hide}}
(defonce editable-code
  (atom "(defn add [& args] (apply + args))
(add 10 20)"))

(load-string @editable-code)

mkvlr16:05:11

or if you want o eval in sci:

(ns scratch
  (:require [nextjournal.clerk :as clerk]
            [nextjournal.clerk.viewer :as viewer]))

(def editor-sync-viewer
  (assoc viewer/viewer-eval-viewer :render-fn '(fn [!code _]
                                                 [:div.bg-neutral-50 [nextjournal.clerk.render.code/editor !code]])))


^{::clerk/sync true
  ::clerk/viewer editor-sync-viewer
  ::clerk/visibility {:code :hide}}
(defonce editable-code
  (atom "(defn add [& args] (apply + args))
(add 10 20)"))

(clerk/eval-cljs-str @editable-code)

rickmoynihan16:05:16

yeah ok it does work but needs some error handling not to also nuke your whole editing environment on a syntax error

mkvlr16:05:42

eval on change isn’t going to be a great ux I think

mkvlr16:05:00

as long as you don’t have a way to avoid errors like hazel

borkdude16:05:32

this is why I used safe-load-string in my example

👀 2
mkvlr16:05:09

here’s eval on click:

(ns scratch
  {::clerk/visibility {:code :hide}}
  (:require [nextjournal.clerk :as clerk]
            [nextjournal.clerk.viewer :as viewer]))

(def editor-sync-viewer
  (assoc viewer/viewer-eval-viewer :render-fn '(fn [!code _]
                                                 [:div.bg-neutral-50 [nextjournal.clerk.render.code/editor !code]])))


^{::clerk/sync true
  ::clerk/viewer editor-sync-viewer}
(defonce editable-code
  (atom "(defn add [& args] (apply + args))
(add 10 20)"))

^{::clerk/sync true}
(defonce result (atom (load-string @editable-code)))

(defn eval! []
  (reset! result (load-string @editable-code)))

(clerk/with-viewer {:render-fn '(fn [_]
                                  [:button.bg-blue-500.p-2.rounded {:on-click #(nextjournal.clerk.render/clerk-eval '(eval!))}
                                   "Eval!"])}
  {})

#_(eval!)

mkvlr16:05:44

@U04V15CAJ right only thing that didn’t work about your example for me is that I didn’t see the eval button

borkdude16:05:58

weird, I did see it, it was right of the input

rickmoynihan16:05:13

It was there — it did render a little strange at the right, so might have been easily missed

mkvlr16:05:57

so what’s the original problem again (sorry for not asking this earlier!)

mkvlr16:05:11

so you want the example from @U04V15CAJ but with on-change instead of on-click?

mkvlr16:05:56

thing there is if you overwrite the on-change handler – you’re in control of swapping the atom state

mkvlr16:05:02

try

(clerk/with-viewer
  {:render-fn
   '(fn [value]
      (let [default-value "(defn foo [x] (+ x 10))
(foo 30)"
            !input (reagent.core/atom default-value)
            !val (reagent.core/atom (load-string @!input))
            on-change (fn [code]
                        (reset! !input code)
                        (prn @!input (load-string @!input))
                        (reset! !val (load-string @!input)))]
        (fn [value]
          [:div
           [:div.flex
            [:div.viewer-code.flex-auto.w-80.mb-2 [nextjournal.clerk.render.code/editor
                                                   !input
                                                   {:on-change on-change}]]
            ]
           [nextjournal.clerk.render/inspect
            @!val]])))}
  nil)

rickmoynihan16:05:01

yeah I think this is the sort of UX I like best… Except perhaps if there’s an error to note also nuke the editor but I think that can be done in the renderer

rickmoynihan17:05:39

Is it possible to put an error boundary in between the sub components

rickmoynihan17:05:27

This seems to work how I’d like:

(ns scratch
  {:nextjournal.clerk/no-cache true}
  (:require [nextjournal.clerk :as clerk]
            [nextjournal.clerk.viewer :as viewer]
            ))

;; # latest

(clerk/with-viewer
  {:render-fn
   '(fn [value]
      (let [default-value "(defn add [& args] (apply + args))
(add 30)"
            !input (reagent.core/atom default-value)
            !val (reagent.core/atom (load-string @!input))
            on-change (fn [code]
                        (reset! !input code)
                        ;;(prn @!input (load-string @!input))
                        (reset! !val (try (load-string @!input)
                                          (catch Error ex
                                            ex))))]
        (fn [value]
          [:div
           [:div.flex
            [:div.viewer-code.flex-auto.w-80.mb-2 [nextjournal.clerk.render.code/editor
                                                   !input
                                                   {:on-change on-change}]]]
           [nextjournal.clerk.render/inspect
            @!val]])))}
  nil)

rickmoynihan17:05:19

this is ace! Thanks! 🙇

🙌 2
rickmoynihan17:05:58

I know it doesn’t work on the JVM but I can live with that

Sam Ritchie17:05:03

weird, in the emacs browser I see this:

Sam Ritchie17:05:11

when I use the backspace key. works fine in safari and brave

mkvlr18:05:30

huh, no idea what the emacs browser does here

rickmoynihan09:05:53

I’m currently struggling to debug an issue with the error handling in the code snippet below:

(ns scratch
  {:nextjournal.clerk/no-cache true}
  (:require [nextjournal.clerk :as clerk]
            [nextjournal.clerk.viewer :as viewer]
            ))

;; # latest

(clerk/with-viewer
  {:render-fn
   '(fn [value]
      (let [default-value "(map [] str)"
            !input (reagent.core/atom default-value)
            !val (reagent.core/atom (load-string @!input))
            on-change (fn [code]
                        (reset! !input code)
                        ;;(prn @!input (load-string @!input))
                        (reset! !val (try (load-string @!input)
                                          (catch Error ex
                                            ex))))]
        (fn [value]
          [:div
           [:div.flex
            [:div.viewer-code.flex-auto.w-80.mb-2 [nextjournal.clerk.render.code/editor
                                                   !input
                                                   {:on-change on-change}]]]
           [nextjournal.clerk.render/inspect
            @!val]])))}
  nil)
Note the intentional erroneous arg order (map [] str)… I don’t understand why this error appears to escape the catch, and nuke the whole component, but syntax errors are caught. Any ideas?

rickmoynihan09:05:11

I suspect it’s to do with the root exception type in sci!?! Not sure how that works in sci, is it inherited from the host, so here presumably cljs? Also it seems that the initialisation is run twice :thinking_face:

Andrea09:05:57

the initial load-string of the default-value is not wrapped in a try/catch maybe?

borkdude09:05:07

it escapes the catch because of laziness?

rickmoynihan09:05:56

> the initial load-string of the default-value is not wrapped in a try/catch maybe? oh sorry I have a newer version that fixes that… but it still happens > it escapes the catch because of laziness? :thinking_face:

borkdude09:05:33

$ clj
Clojure 1.10.3
user=> (try (map [] str) (catch Exception e 1))
Error printing return value (IllegalArgumentException) at clojure.lang.RT/seqFrom (RT.java:557).
Don't know how to create ISeq from: clojure.core$str
(user=>

👍 2
rickmoynihan09:05:41

I should have thought of that!

rickmoynihan11:05:28

This variation seems to fix the issue nicely:

(def editor-sync-viewer
  {:evaluator :sci
   :render-fn
   '(fn [value]
      (let [safe-load-string (fn [s]
                               (try
                                 (let [ret (load-string s)]
                                   (if (seq? ret)
                                     (try
                                       (first ret)
                                       ret
                                       (catch :default e
                                         e))))
                                 (catch :default e
                                   e)))

            !input (reagent.core/atom value)
            !val (reagent.core/atom (safe-load-string @!input))
            on-change (fn [code]
                        (reset! !input code)
                        (reset! !val (safe-load-string @!input)))]
        (fn [value]
          [:div
           "stuff"
           [:div.flex
            [:div.viewer-code.flex-auto.w-80.mb-2 [nextjournal.clerk.render.code/editor
                                                   !input
                                                   {:on-change on-change}]]]
           [:em "=> "]
           [nextjournal.clerk.render/inspect
            @!val]])))})

(clerk/with-viewer editor-sync-viewer
  "(map inc [1 2 3 4])")