From 67e55579267d07a75952bcabfcbce47e50b2be18 Mon Sep 17 00:00:00 2001 From: Andrea Amantini Date: Mon, 31 Oct 2022 18:06:01 +0100 Subject: [PATCH 01/13] First take at hookifying code viewer inline in render ns --- src/nextjournal/clerk/render.cljs | 82 ++++++++++++++++++++++++++++++- 1 file changed, 81 insertions(+), 1 deletion(-) diff --git a/src/nextjournal/clerk/render.cljs b/src/nextjournal/clerk/render.cljs index 8de21d0bf..bedc86ff5 100644 --- a/src/nextjournal/clerk/render.cljs +++ b/src/nextjournal/clerk/render.cljs @@ -3,6 +3,14 @@ ["react" :as react] ["react-dom/client" :as react-client] ["use-sync-external-store/shim" :refer [useSyncExternalStore]] + + ;; TODO: move to own ns + ["@codemirror/language" :refer [syntaxHighlighting HighlightStyle]] + ["@codemirror/state" :refer [EditorState]] + ["@codemirror/view" :refer [EditorView]] + ["@lezer/highlight" :refer [tags]] + [nextjournal.clojure-mode :as cm-clj] + [applied-science.js-interop :as j] [cljs.reader] [clojure.string :as str] @@ -765,7 +773,79 @@ default-loading-view)))) (def render-mathjax mathjax/viewer) -(def render-code code/viewer) +#_(def render-code code/viewer) + +;; code viewer +(def cm6-theme + (.theme EditorView + (j/lit {"&.cm-focused" {:outline "none"} + ".cm-line" {:padding "0" + :line-height "1.6" + :font-size "15px" + :font-family "\"Fira Mono\", monospace"} + ".cm-matchingBracket" {:border-bottom "1px solid var(--teal-color)" + :color "inherit"} + + ;; only show cursor when focused + ".cm-cursor" {:visibility "hidden"} + "&.cm-focused .cm-cursor" {:visibility "visible" + :animation "steps(1) cm-blink 1.2s infinite"} + "&.cm-focused .cm-selectionBackground" {:background-color "Highlight"} + ".cm-tooltip" {:border "1px solid rgba(0,0,0,.1)" + :border-radius "3px" + :overflow "hidden"} + ".cm-tooltip > ul > li" {:padding "3px 10px 3px 0 !important"} + ".cm-tooltip > ul > li:first-child" {:border-top-left-radius "3px" + :border-top-right-radius "3px"}}))) + +(def highlight-style + (.define HighlightStyle + (clj->js [{:tag (.-meta tags) :class "cmt-meta"} + {:tag (.-link tags) :class "cmt-link"} + {:tag (.-heading tags) :class "cmt-heading"} + {:tag (.-emphasis tags) :class "cmt-italic"} + {:tag (.-strong tags) :class "cmt-strong"} + {:tag (.-strikethrough tags) :class "cmt-strikethrough"} + {:tag (.-keyword tags) :class "cmt-keyword"} + {:tag (.-atom tags) :class "cmt-atom"} + {:tag (.-bool tags) :class "cmt-bool"} + {:tag (.-url tags) :class "cmt-url"} + {:tag (.-contentSeparator tags) :class "cmt-contentSeparator"} + {:tag (.-labelName tags) :class "cmt-labelName"} + {:tag (.-literal tags) :class "cmt-literal"} + {:tag (.-inserted tags) :class "cmt-inserted"} + {:tag (.-string tags) :class "cmt-string"} + {:tag (.-deleted tags) :class "cmt-deleted"} + {:tag (.-regexp tags) :class "cmt-regexp"} + {:tag (.-escape tags) :class "cmt-escape"} + {:tag (.. tags (special (.-string tags))) :class "cmt-string"} + {:tag (.. tags (definition (.-variableName tags))) :class "cmt-variableName"} + {:tag (.. tags (local (.-variableName tags))) :class "cmt-variableName"} + {:tag (.-typeName tags) :class "cmt-typeName"} + {:tag (.-namespace tags) :class "cmt-namespace"} + {:tag (.-className tags) :class "cmt-className"} + {:tag (.. tags (special (.-variableName tags))) :class "cmt-variableName"} + {:tag (.-macroName tags) :class "cmt-macroName"} + {:tag (.. tags (definition (.-propertyName tags))) :class "cmt-propertyName"} + {:tag (.-comment tags) :class "cmt-comment"} + {:tag (.-invalid tags) :class "cmt-invalid"}]))) + +(def ext #js [cm-clj/default-extensions + (syntaxHighlighting highlight-style) + (.. EditorView -editable (of false)) + cm6-theme]) + +(defn render-code [value] + (let [ref (use-ref nil)] + (use-effect (fn [] + (js/console.log :mounting @ref) + (let [^js editor-view + (EditorView. #js {:state (.create EditorState #js {:doc value + :extensions ext}) + :parent @ref})] + (fn [] (js/console.log :unmounting editor-view) + (.destroy editor-view))))) + [:div {:ref ref}])) (def expand-icon [:svg {:xmlns "http://www.w3.org/2000/svg" :viewBox "0 0 20 20" :fill "currentColor" :width 12 :height 12} From f6fafd2f4b0adb21a9ef0788818702024d32d303 Mon Sep 17 00:00:00 2001 From: Andrea Amantini Date: Wed, 2 Nov 2022 10:17:06 +0100 Subject: [PATCH 02/13] Organize --- resources/viewer-js-hash | 2 +- src/nextjournal/clerk/render.cljs | 80 ++------------------------ src/nextjournal/clerk/render/code.cljs | 72 +++++++++++++++++++++++ 3 files changed, 79 insertions(+), 75 deletions(-) create mode 100644 src/nextjournal/clerk/render/code.cljs diff --git a/resources/viewer-js-hash b/resources/viewer-js-hash index 89de9656f..1085eeabb 100644 --- a/resources/viewer-js-hash +++ b/resources/viewer-js-hash @@ -1 +1 @@ -3nPjsVv13n4g6ceiJx6JUk9jjawS \ No newline at end of file +3vrCupwkk8cayJ4L2zjn8sZ66ibz \ No newline at end of file diff --git a/src/nextjournal/clerk/render.cljs b/src/nextjournal/clerk/render.cljs index bedc86ff5..eca1a1a14 100644 --- a/src/nextjournal/clerk/render.cljs +++ b/src/nextjournal/clerk/render.cljs @@ -3,19 +3,12 @@ ["react" :as react] ["react-dom/client" :as react-client] ["use-sync-external-store/shim" :refer [useSyncExternalStore]] - - ;; TODO: move to own ns - ["@codemirror/language" :refer [syntaxHighlighting HighlightStyle]] - ["@codemirror/state" :refer [EditorState]] - ["@codemirror/view" :refer [EditorView]] - ["@lezer/highlight" :refer [tags]] - [nextjournal.clojure-mode :as cm-clj] - [applied-science.js-interop :as j] [cljs.reader] [clojure.string :as str] [goog.object] [goog.string :as gstring] + [nextjournal.clerk.render.code :as render.code] [nextjournal.clerk.viewer :as viewer] [nextjournal.markdown.transform :as md.transform] [nextjournal.ui.components.icon :as icon] @@ -298,6 +291,8 @@ (constructor [this ^js props] (super props) + (js/console.log :Error props (type props) (j/get props :!error) ) + (set! !error (j/get props :!error)) (set! (.-state this) #js{:error @!error})) Object @@ -775,76 +770,13 @@ (def render-mathjax mathjax/viewer) #_(def render-code code/viewer) -;; code viewer -(def cm6-theme - (.theme EditorView - (j/lit {"&.cm-focused" {:outline "none"} - ".cm-line" {:padding "0" - :line-height "1.6" - :font-size "15px" - :font-family "\"Fira Mono\", monospace"} - ".cm-matchingBracket" {:border-bottom "1px solid var(--teal-color)" - :color "inherit"} - - ;; only show cursor when focused - ".cm-cursor" {:visibility "hidden"} - "&.cm-focused .cm-cursor" {:visibility "visible" - :animation "steps(1) cm-blink 1.2s infinite"} - "&.cm-focused .cm-selectionBackground" {:background-color "Highlight"} - ".cm-tooltip" {:border "1px solid rgba(0,0,0,.1)" - :border-radius "3px" - :overflow "hidden"} - ".cm-tooltip > ul > li" {:padding "3px 10px 3px 0 !important"} - ".cm-tooltip > ul > li:first-child" {:border-top-left-radius "3px" - :border-top-right-radius "3px"}}))) - -(def highlight-style - (.define HighlightStyle - (clj->js [{:tag (.-meta tags) :class "cmt-meta"} - {:tag (.-link tags) :class "cmt-link"} - {:tag (.-heading tags) :class "cmt-heading"} - {:tag (.-emphasis tags) :class "cmt-italic"} - {:tag (.-strong tags) :class "cmt-strong"} - {:tag (.-strikethrough tags) :class "cmt-strikethrough"} - {:tag (.-keyword tags) :class "cmt-keyword"} - {:tag (.-atom tags) :class "cmt-atom"} - {:tag (.-bool tags) :class "cmt-bool"} - {:tag (.-url tags) :class "cmt-url"} - {:tag (.-contentSeparator tags) :class "cmt-contentSeparator"} - {:tag (.-labelName tags) :class "cmt-labelName"} - {:tag (.-literal tags) :class "cmt-literal"} - {:tag (.-inserted tags) :class "cmt-inserted"} - {:tag (.-string tags) :class "cmt-string"} - {:tag (.-deleted tags) :class "cmt-deleted"} - {:tag (.-regexp tags) :class "cmt-regexp"} - {:tag (.-escape tags) :class "cmt-escape"} - {:tag (.. tags (special (.-string tags))) :class "cmt-string"} - {:tag (.. tags (definition (.-variableName tags))) :class "cmt-variableName"} - {:tag (.. tags (local (.-variableName tags))) :class "cmt-variableName"} - {:tag (.-typeName tags) :class "cmt-typeName"} - {:tag (.-namespace tags) :class "cmt-namespace"} - {:tag (.-className tags) :class "cmt-className"} - {:tag (.. tags (special (.-variableName tags))) :class "cmt-variableName"} - {:tag (.-macroName tags) :class "cmt-macroName"} - {:tag (.. tags (definition (.-propertyName tags))) :class "cmt-propertyName"} - {:tag (.-comment tags) :class "cmt-comment"} - {:tag (.-invalid tags) :class "cmt-invalid"}]))) - -(def ext #js [cm-clj/default-extensions - (syntaxHighlighting highlight-style) - (.. EditorView -editable (of false)) - cm6-theme]) - (defn render-code [value] (let [ref (use-ref nil)] (use-effect (fn [] (js/console.log :mounting @ref) - (let [^js editor-view - (EditorView. #js {:state (.create EditorState #js {:doc value - :extensions ext}) - :parent @ref})] - (fn [] (js/console.log :unmounting editor-view) - (.destroy editor-view))))) + (let [^js ev (render.code/cm-view value @ref)] + (fn [] (js/console.log :unmounting ev) + (.destroy ev))))) [:div {:ref ref}])) (def expand-icon diff --git a/src/nextjournal/clerk/render/code.cljs b/src/nextjournal/clerk/render/code.cljs new file mode 100644 index 000000000..a3f8c16eb --- /dev/null +++ b/src/nextjournal/clerk/render/code.cljs @@ -0,0 +1,72 @@ +(ns nextjournal.clerk.render.code + (:require ["@codemirror/language" :refer [syntaxHighlighting HighlightStyle]] + ["@lezer/highlight" :refer [tags]] + ["@codemirror/state" :refer [EditorState]] + ["@codemirror/view" :refer [EditorView]] + [applied-science.js-interop :as j] + [nextjournal.clojure-mode :as clojure-mode])) + +;; code viewer +(def cm-theme + (.theme EditorView + (j/lit {"&.cm-focused" {:outline "none"} + ".cm-line" {:padding "0" + :line-height "1.6" + :font-size "15px" + :font-family "\"Fira Mono\", monospace"} + ".cm-matchingBracket" {:border-bottom "1px solid var(--teal-color)" + :color "inherit"} + + ;; only show cursor when focused + ".cm-cursor" {:visibility "hidden"} + "&.cm-focused .cm-cursor" {:visibility "visible" + :animation "steps(1) cm-blink 1.2s infinite"} + "&.cm-focused .cm-selectionBackground" {:background-color "Highlight"} + ".cm-tooltip" {:border "1px solid rgba(0,0,0,.1)" + :border-radius "3px" + :overflow "hidden"} + ".cm-tooltip > ul > li" {:padding "3px 10px 3px 0 !important"} + ".cm-tooltip > ul > li:first-child" {:border-top-left-radius "3px" + :border-top-right-radius "3px"}}))) + +(def cm-highlight-style + (.define HighlightStyle + (clj->js [{:tag (.-meta tags) :class "cmt-meta"} + {:tag (.-link tags) :class "cmt-link"} + {:tag (.-heading tags) :class "cmt-heading"} + {:tag (.-emphasis tags) :class "cmt-italic"} + {:tag (.-strong tags) :class "cmt-strong"} + {:tag (.-strikethrough tags) :class "cmt-strikethrough"} + {:tag (.-keyword tags) :class "cmt-keyword"} + {:tag (.-atom tags) :class "cmt-atom"} + {:tag (.-bool tags) :class "cmt-bool"} + {:tag (.-url tags) :class "cmt-url"} + {:tag (.-contentSeparator tags) :class "cmt-contentSeparator"} + {:tag (.-labelName tags) :class "cmt-labelName"} + {:tag (.-literal tags) :class "cmt-literal"} + {:tag (.-inserted tags) :class "cmt-inserted"} + {:tag (.-string tags) :class "cmt-string"} + {:tag (.-deleted tags) :class "cmt-deleted"} + {:tag (.-regexp tags) :class "cmt-regexp"} + {:tag (.-escape tags) :class "cmt-escape"} + {:tag (.. tags (special (.-string tags))) :class "cmt-string"} + {:tag (.. tags (definition (.-variableName tags))) :class "cmt-variableName"} + {:tag (.. tags (local (.-variableName tags))) :class "cmt-variableName"} + {:tag (.-typeName tags) :class "cmt-typeName"} + {:tag (.-namespace tags) :class "cmt-namespace"} + {:tag (.-className tags) :class "cmt-className"} + {:tag (.. tags (special (.-variableName tags))) :class "cmt-variableName"} + {:tag (.-macroName tags) :class "cmt-macroName"} + {:tag (.. tags (definition (.-propertyName tags))) :class "cmt-propertyName"} + {:tag (.-comment tags) :class "cmt-comment"} + {:tag (.-invalid tags) :class "cmt-invalid"}]))) + +(def cm-extensions + #js [clojure-mode/default-extensions + (syntaxHighlighting cm-highlight-style) + (.. EditorView -editable (of false)) + cm-theme]) + +(defn cm-view [doc parent] + (EditorView. (j/obj :state (.create EditorState (j/obj :doc doc :extensions cm-extensions)) + :parent parent))) From 5d195762564fe4e8c53c76ad58dfefa1d4a0deeb Mon Sep 17 00:00:00 2001 From: Andrea Amantini Date: Wed, 2 Nov 2022 10:21:23 +0100 Subject: [PATCH 03/13] Add deps --- resources/viewer-js-hash | 2 +- src/nextjournal/clerk/render.cljs | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/resources/viewer-js-hash b/resources/viewer-js-hash index 1085eeabb..cc4001af6 100644 --- a/resources/viewer-js-hash +++ b/resources/viewer-js-hash @@ -1 +1 @@ -3vrCupwkk8cayJ4L2zjn8sZ66ibz \ No newline at end of file +2sf4i4PHjc72mNuvfg2TNpRGyE2V \ No newline at end of file diff --git a/src/nextjournal/clerk/render.cljs b/src/nextjournal/clerk/render.cljs index eca1a1a14..eb32b9ed1 100644 --- a/src/nextjournal/clerk/render.cljs +++ b/src/nextjournal/clerk/render.cljs @@ -773,10 +773,11 @@ (defn render-code [value] (let [ref (use-ref nil)] (use-effect (fn [] - (js/console.log :mounting @ref) - (let [^js ev (render.code/cm-view value @ref)] - (fn [] (js/console.log :unmounting ev) - (.destroy ev))))) + (js/console.log :mounting @ref :with value) + (let [^js editor-view (render.code/cm-view value @ref)] + (fn [] + (js/console.log :unmounting value) + (.destroy editor-view)))) [value]) [:div {:ref ref}])) (def expand-icon From 2bea2a3769a8a563ed886d59612b273df2ff0b6f Mon Sep 17 00:00:00 2001 From: Andrea Amantini Date: Wed, 2 Nov 2022 10:24:02 +0100 Subject: [PATCH 04/13] Remove logs --- resources/viewer-js-hash | 2 +- src/nextjournal/clerk/render.cljs | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/resources/viewer-js-hash b/resources/viewer-js-hash index cc4001af6..85a71bb5f 100644 --- a/resources/viewer-js-hash +++ b/resources/viewer-js-hash @@ -1 +1 @@ -2sf4i4PHjc72mNuvfg2TNpRGyE2V \ No newline at end of file +aC3hTYUh5q4UYggeyfTiocJVqx9 \ No newline at end of file diff --git a/src/nextjournal/clerk/render.cljs b/src/nextjournal/clerk/render.cljs index eb32b9ed1..c3d66dec6 100644 --- a/src/nextjournal/clerk/render.cljs +++ b/src/nextjournal/clerk/render.cljs @@ -773,11 +773,8 @@ (defn render-code [value] (let [ref (use-ref nil)] (use-effect (fn [] - (js/console.log :mounting @ref :with value) (let [^js editor-view (render.code/cm-view value @ref)] - (fn [] - (js/console.log :unmounting value) - (.destroy editor-view)))) [value]) + #(.destroy editor-view))) [value]) [:div {:ref ref}])) (def expand-icon From be8ea714a431d7179f9e341a91933d7eb01dd171 Mon Sep 17 00:00:00 2001 From: Andrea Amantini Date: Wed, 2 Nov 2022 10:25:31 +0100 Subject: [PATCH 05/13] Cleanup --- resources/viewer-js-hash | 2 +- src/nextjournal/clerk/render.cljs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/resources/viewer-js-hash b/resources/viewer-js-hash index 85a71bb5f..5a61fd4b8 100644 --- a/resources/viewer-js-hash +++ b/resources/viewer-js-hash @@ -1 +1 @@ -aC3hTYUh5q4UYggeyfTiocJVqx9 \ No newline at end of file +2nVS3zGQb87HCy21WJgfNtHzk74q \ No newline at end of file diff --git a/src/nextjournal/clerk/render.cljs b/src/nextjournal/clerk/render.cljs index c3d66dec6..ec987f4b3 100644 --- a/src/nextjournal/clerk/render.cljs +++ b/src/nextjournal/clerk/render.cljs @@ -768,7 +768,6 @@ default-loading-view)))) (def render-mathjax mathjax/viewer) -#_(def render-code code/viewer) (defn render-code [value] (let [ref (use-ref nil)] From 69a1fec2d3703cb8cd34b9c67427263dd960d66e Mon Sep 17 00:00:00 2001 From: Martin Kavalar Date: Thu, 10 Nov 2022 12:42:22 +0100 Subject: [PATCH 06/13] use-state for folded code state --- src/nextjournal/clerk/render.cljs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nextjournal/clerk/render.cljs b/src/nextjournal/clerk/render.cljs index 450996e90..6b4dbc238 100644 --- a/src/nextjournal/clerk/render.cljs +++ b/src/nextjournal/clerk/render.cljs @@ -807,7 +807,7 @@ [:path {:fill-rule "evenodd" :d "M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" :clip-rule "evenodd"}]]) (defn render-folded-code [code-string] - (r/with-let [!hidden? (r/atom true)] + (let [!hidden? (use-state true)] (if @!hidden? [:div.relative.pl-12.font-sans.text-slate-400.cursor-pointer.flex.overflow-y-hidden.group [:span.hover:text-slate-500 From 463d886f8c1107c1e9821b0ecef1168a6104d280 Mon Sep 17 00:00:00 2001 From: Martin Kavalar Date: Thu, 10 Nov 2022 12:56:14 +0100 Subject: [PATCH 07/13] Move hooks to separate ns --- src/nextjournal/clerk/render.cljs | 209 ++++-------------------- src/nextjournal/clerk/render/code.cljs | 9 + src/nextjournal/clerk/render/hooks.cljs | 144 ++++++++++++++++ 3 files changed, 185 insertions(+), 177 deletions(-) create mode 100644 src/nextjournal/clerk/render/hooks.cljs diff --git a/src/nextjournal/clerk/render.cljs b/src/nextjournal/clerk/render.cljs index 6b4dbc238..5af4f4361 100644 --- a/src/nextjournal/clerk/render.cljs +++ b/src/nextjournal/clerk/render.cljs @@ -2,21 +2,20 @@ (:require ["d3-require" :as d3-require] ["react" :as react] ["react-dom/client" :as react-client] - ["use-sync-external-store/shim" :refer [useSyncExternalStore]] [applied-science.js-interop :as j] [cljs.reader] [clojure.string :as str] [editscript.core :as editscript] [goog.object] [goog.string :as gstring] - [nextjournal.clerk.render.code :as render.code] + [nextjournal.clerk.render.code :as code] + [nextjournal.clerk.render.hooks :as hooks] [nextjournal.clerk.viewer :as viewer] [nextjournal.markdown.transform :as md.transform] [nextjournal.ui.components.icon :as icon] [nextjournal.ui.components.motion :as motion] [nextjournal.ui.components.navbar :as navbar] [nextjournal.view.context :as view-context] - [nextjournal.viewer.code :as code] [nextjournal.viewer.katex :as katex] [nextjournal.viewer.mathjax :as mathjax] [reagent.core :as r] @@ -25,120 +24,6 @@ [sci.core :as sci] [shadow.cljs.modern :refer [defclass]])) -;; a type for wrapping react/useState to support reset! and swap! -(deftype WrappedState [st] - IIndexed - (-nth [coll i] (aget st i)) - (-nth [coll i nf] (or (aget st i) nf)) - IDeref - (-deref [^js this] (aget st 0)) - IReset - (-reset! [^js this new-value] - ;; `constantly` here ensures that if we reset state to a fn, - ;; it is stored as-is and not applied to prev value. - ((aget st 1) (constantly new-value))) - ISwap - (-swap! [this f] ((aget st 1) f)) - (-swap! [this f a] ((aget st 1) #(f % a))) - (-swap! [this f a b] ((aget st 1) #(f % a b))) - (-swap! [this f a b xs] ((aget st 1) #(apply f % a b xs)))) - -(defn- as-array [x] (cond-> x (not (array? x)) to-array)) - -(defn use-memo - "React hook: useMemo. Defaults to an empty `deps` array." - ([f] (react/useMemo f #js[])) - ([f deps] (react/useMemo f (as-array deps)))) - -(defn use-callback - "React hook: useCallback. Defaults to an empty `deps` array." - ([x] (use-callback x #js [])) - ([x deps] (react/useCallback x (to-array deps)))) - -(defn- wrap-effect - ;; utility for wrapping function to return `js/undefined` for non-functions - [f] #(let [v (f)] (if (fn? v) v js/undefined))) - -(defn use-effect - "React hook: useEffect. Defaults to an empty `deps` array. - Wraps `f` to return js/undefined for any non-function value." - ([f] (react/useEffect (wrap-effect f) #js[])) - ([f deps] (react/useEffect (wrap-effect f) (as-array deps)))) - -(defn use-state - "React hook: useState. Can be used like react/useState but also behaves like an atom." - [init] - (WrappedState. (react/useState init))) - -(defn- specify-atom! [ref-obj] - (specify! ref-obj - IDeref - (-deref [^js this] (.-current this)) - IReset - (-reset! [^js this new-value] (set! (.-current this) new-value)) - ISwap - (-swap! - ([o f] (reset! o (f o))) - ([o f a] (reset! o (f o a))) - ([o f a b] (reset! o (f o a b))) - ([o f a b xs] (reset! o (apply f o a b xs)))))) - -(defn use-ref - "React hook: useRef. Can also be used like an atom." - ([] (use-ref nil)) - ([init] (specify-atom! (react/useRef init)))) - -(defn- eval-fn - "Invoke (f x) if f is a function, otherwise return f" - [f x] - (if (fn? f) - (f x) - f)) - -(defn use-force-update [] - (-> (react/useReducer inc 0) - (aget 1))) - -(defn use-state-with-deps - ;; see https://github.com/peterjuras/use-state-with-deps/blob/main/src/index.ts - "React hook: like `use-state` but will reset state to `init` when `deps` change. - - init may be a function, receiving previous state - - deps will be compared using clojure =" - [init deps] - (let [!state (use-ref - (use-memo - #(eval-fn init nil))) - !prev-deps (use-ref deps) - _ (when-not (= deps @!prev-deps) - (reset! !state (eval-fn init @!state)) - (reset! !prev-deps deps)) - force-update! (use-force-update) - update-fn (use-callback - (fn [x] - (let [prev-state @!state - next-state (eval-fn x prev-state)] - (when (not= prev-state next-state) - (reset! !state next-state) - (force-update!)) - next-state)))] - (WrappedState. #js[@!state update-fn]))) - - -(defn use-sync-external-store [subscribe get-snapshot] - (useSyncExternalStore subscribe get-snapshot)) - -(defn use-watch - "Hook for reading value of an IWatchable. Compatible with reading Reagent reactions non-reactively." - [x] - (let [id (use-callback #js{})] - (use-sync-external-store - (use-callback - (fn [changed!] - (add-watch x id (fn [_ _ _ _] (changed!))) - #(remove-watch x id)) - #js[x]) - #(binding [reagent.ratom/*ratom-context* nil] @x)))) - (r/set-default-compiler! (r/create-compiler {:function-components true})) (declare inspect inspect-presented reagent-viewer html html-viewer) @@ -350,12 +235,6 @@ (def default-loading-view "Loading...") -(defn use-error-handler [] - (let [[_ set-error] (use-state nil)] - (use-callback (fn [error] - (set-error (fn [] (throw error)))) - [set-error]))) - ;; TODO: drop this (defn read-string [s] (js/nextjournal.clerk.sci_env.read-string s)) @@ -377,28 +256,28 @@ true (-> viewer/assign-expanded-at (get :nextjournal/expanded-at {})))) (defn render-result [{:as result :nextjournal/keys [fetch-opts hash presented]} {:as opts :keys [auto-expand-results?]}] - (let [!desc (use-state-with-deps presented [hash]) - !expanded-at (use-state (when (map? @!desc) - (->expanded-at auto-expand-results? @!desc))) - fetch-fn (use-callback (when fetch-opts - (fn [opts] - (.then (fetch! fetch-opts opts) - (fn [more] - (swap! !desc viewer/merge-presentations more opts) - (swap! !expanded-at #(merge (->expanded-at auto-expand-results? @!desc) %)))))) - [hash]) - on-key-down (use-callback (fn [event] - (if (.-altKey event) - (swap! !expanded-at assoc :prompt-multi-expand? true) - (swap! !expanded-at dissoc :prompt-multi-expand?)))) - on-key-up (use-callback #(swap! !expanded-at dissoc :prompt-multi-expand?)) - ref-fn (use-callback #(if % - (when (exists? js/document) - (js/document.addEventListener "keydown" on-key-down) - (js/document.addEventListener "keyup" on-key-up)) - (when (exists? js/document) - (js/document.removeEventListener "keydown" on-key-down) - (js/document.removeEventListener "up" on-key-up))))] + (let [!desc (hooks/use-state-with-deps presented [hash]) + !expanded-at (hooks/use-state (when (map? @!desc) + (->expanded-at auto-expand-results? @!desc))) + fetch-fn (hooks/use-callback (when fetch-opts + (fn [opts] + (.then (fetch! fetch-opts opts) + (fn [more] + (swap! !desc viewer/merge-presentations more opts) + (swap! !expanded-at #(merge (->expanded-at auto-expand-results? @!desc) %)))))) + [hash]) + on-key-down (hooks/use-callback (fn [event] + (if (.-altKey event) + (swap! !expanded-at assoc :prompt-multi-expand? true) + (swap! !expanded-at dissoc :prompt-multi-expand?)))) + on-key-up (hooks/use-callback #(swap! !expanded-at dissoc :prompt-multi-expand?)) + ref-fn (hooks/use-callback #(if % + (when (exists? js/document) + (js/document.addEventListener "keydown" on-key-down) + (js/document.addEventListener "keyup" on-key-up)) + (when (exists? js/document) + (js/document.removeEventListener "keydown" on-key-down) + (js/document.removeEventListener "up" on-key-up))))] (when @!desc [view-context/provide {:fetch-fn fetch-fn} [:> ErrorBoundary {:hash hash} @@ -461,8 +340,8 @@ [:span.group.hover:bg-indigo-100.rounded-sm.hover:shadow.cursor-pointer {:class (when multi-expand? "bg-indigo-100 shadow ") :on-click (partial toggle-expanded !expanded-at path) - :on-mouse-enter #(swap! !expanded-at assoc :hover-path path) - :on-mouse-leave #(swap! !expanded-at dissoc :hover-path)} + :on-mohooks/use-enter #(swap! !expanded-at assoc :hover-path path) + :on-mohooks/use-leave #(swap! !expanded-at dissoc :hover-path)} [:span.text-slate-400.group-hover:text-indigo-700 {:class (when multi-expand? "text-indigo-700 ")} [triangle expanded?]] @@ -743,35 +622,16 @@ ;; TODO: remove (def reagent-viewer render-reagent) -(defn use-promise - "React hook which resolves a promise and handles errors." - [p] - (let [handle-error (use-error-handler) - !state (use-state nil)] - (use-effect (fn [] - (-> p - (.then #(reset! !state %)) - (.catch handle-error))) - #js []) - @!state)) - -(defn ^js use-d3-require [package] - (let [p (react/useMemo #(apply d3-require/require - (cond-> package - (string? package) - list)) - #js[(str package)])] - (use-promise p))) (defn with-d3-require [{:keys [package loading-view] :or {loading-view default-loading-view}} f] - (if-let [package (use-d3-require package)] + (if-let [package (hooks/use-d3-require package)] (f package) loading-view)) (defn render-vega-lite [value] - (let [handle-error (use-error-handler) - vega-embed (use-d3-require "vega-embed@6.11.1") + (let [handle-error (hooks/use-error-handler) + vega-embed (hooks/use-d3-require "vega-embed@6.11.1") ref-fn (react/useCallback #(when % (-> (.embed vega-embed % (clj->js (dissoc value :embed/opts)) (clj->js (:embed/opts value {}))) (.catch handle-error))) @@ -783,7 +643,7 @@ default-loading-view)))) (defn render-plotly [value] - (let [plotly (use-d3-require "plotly.js-dist@2.15.1") + (let [plotly (hooks/use-d3-require "plotly.js-dist@2.15.1") ref-fn (react/useCallback #(when % (.newPlot plotly % (clj->js value))) #js[value plotly])] @@ -795,19 +655,14 @@ (def render-mathjax mathjax/viewer) -(defn render-code [value] - (let [ref (use-ref nil)] - (use-effect (fn [] - (let [^js editor-view (render.code/cm-view value @ref)] - #(.destroy editor-view))) [value]) - [:div {:ref ref}])) +(def render-code code/render-code) (def expand-icon [:svg {:xmlns "http://www.w3.org/2000/svg" :viewBox "0 0 20 20" :fill "currentColor" :width 12 :height 12} [:path {:fill-rule "evenodd" :d "M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" :clip-rule "evenodd"}]]) (defn render-folded-code [code-string] - (let [!hidden? (use-state true)] + (let [!hidden? (hooks/use-state true)] (if @!hidden? [:div.relative.pl-12.font-sans.text-slate-400.cursor-pointer.flex.overflow-y-hidden.group [:span.hover:text-slate-500 diff --git a/src/nextjournal/clerk/render/code.cljs b/src/nextjournal/clerk/render/code.cljs index a3f8c16eb..bf75c3e40 100644 --- a/src/nextjournal/clerk/render/code.cljs +++ b/src/nextjournal/clerk/render/code.cljs @@ -3,6 +3,7 @@ ["@lezer/highlight" :refer [tags]] ["@codemirror/state" :refer [EditorState]] ["@codemirror/view" :refer [EditorView]] + [nextjournal.clerk.render.hooks :as hooks] [applied-science.js-interop :as j] [nextjournal.clojure-mode :as clojure-mode])) @@ -70,3 +71,11 @@ (defn cm-view [doc parent] (EditorView. (j/obj :state (.create EditorState (j/obj :doc doc :extensions cm-extensions)) :parent parent))) + +(defn render-code [value] + (let [ref (hooks/use-ref nil)] + (hooks/use-effect (fn [] + (let [^js editor-view (cm-view value @ref)] + #(.destroy editor-view))) [value]) + [:div {:ref ref}])) + diff --git a/src/nextjournal/clerk/render/hooks.cljs b/src/nextjournal/clerk/render/hooks.cljs new file mode 100644 index 000000000..4bd3a1120 --- /dev/null +++ b/src/nextjournal/clerk/render/hooks.cljs @@ -0,0 +1,144 @@ +(ns nextjournal.clerk.render.hooks + (:require ["d3-require" :as d3-require] + ["react" :as react] + ["use-sync-external-store/shim" :refer [useSyncExternalStore]])) + +;; a type for wrapping react/useState to support reset! and swap! +(deftype WrappedState [st] + IIndexed + (-nth [coll i] (aget st i)) + (-nth [coll i nf] (or (aget st i) nf)) + IDeref + (-deref [^js this] (aget st 0)) + IReset + (-reset! [^js this new-value] + ;; `constantly` here ensures that if we reset state to a fn, + ;; it is stored as-is and not applied to prev value. + ((aget st 1) (constantly new-value))) + ISwap + (-swap! [this f] ((aget st 1) f)) + (-swap! [this f a] ((aget st 1) #(f % a))) + (-swap! [this f a b] ((aget st 1) #(f % a b))) + (-swap! [this f a b xs] ((aget st 1) #(apply f % a b xs)))) + +(defn- as-array [x] (cond-> x (not (array? x)) to-array)) + +(defn use-memo + "React hook: useMemo. Defaults to an empty `deps` array." + ([f] (react/useMemo f #js[])) + ([f deps] (react/useMemo f (as-array deps)))) + +(defn use-callback + "React hook: useCallback. Defaults to an empty `deps` array." + ([x] (use-callback x #js [])) + ([x deps] (react/useCallback x (to-array deps)))) + +(defn- wrap-effect + ;; utility for wrapping function to return `js/undefined` for non-functions + [f] #(let [v (f)] (if (fn? v) v js/undefined))) + +(defn use-effect + "React hook: useEffect. Defaults to an empty `deps` array. + Wraps `f` to return js/undefined for any non-function value." + ([f] (react/useEffect (wrap-effect f) #js[])) + ([f deps] (react/useEffect (wrap-effect f) (as-array deps)))) + +(defn use-state + "React hook: useState. Can be used like react/useState but also behaves like an atom." + [init] + (WrappedState. (react/useState init))) + +(defn- specify-atom! [ref-obj] + (specify! ref-obj + IDeref + (-deref [^js this] (.-current this)) + IReset + (-reset! [^js this new-value] (set! (.-current this) new-value)) + ISwap + (-swap! + ([o f] (reset! o (f o))) + ([o f a] (reset! o (f o a))) + ([o f a b] (reset! o (f o a b))) + ([o f a b xs] (reset! o (apply f o a b xs)))))) + +(defn use-ref + "React hook: useRef. Can also be used like an atom." + ([] (use-ref nil)) + ([init] (specify-atom! (react/useRef init)))) + +(defn ^:private eval-fn + "Invoke (f x) if f is a function, otherwise return f" + [f x] + (if (fn? f) + (f x) + f)) + +(defn use-force-update [] + (-> (react/useReducer inc 0) + (aget 1))) + +(defn use-state-with-deps + ;; see https://github.com/peterjuras/use-state-with-deps/blob/main/src/index.ts + "React hook: like `use-state` but will reset state to `init` when `deps` change. + - init may be a function, receiving previous state + - deps will be compared using clojure =" + [init deps] + (let [!state (use-ref + (use-memo + #(eval-fn init nil))) + !prev-deps (use-ref deps) + _ (when-not (= deps @!prev-deps) + (reset! !state (eval-fn init @!state)) + (reset! !prev-deps deps)) + force-update! (use-force-update) + update-fn (use-callback + (fn [x] + (let [prev-state @!state + next-state (eval-fn x prev-state)] + (when (not= prev-state next-state) + (reset! !state next-state) + (force-update!)) + next-state)))] + (WrappedState. #js[@!state update-fn]))) + + +(defn use-sync-external-store [subscribe get-snapshot] + (useSyncExternalStore subscribe get-snapshot)) + +(defn use-watch + "Hook for reading value of an IWatchable. Compatible with reading Reagent reactions non-reactively." + [x] + (let [id (use-callback #js{})] + (use-sync-external-store + (use-callback + (fn [changed!] + (add-watch x id (fn [_ _ _ _] (changed!))) + #(remove-watch x id)) + #js[x]) + #(binding [reagent.ratom/*ratom-context* nil] @x)))) + +(defn use-error-handler [] + (let [[_ set-error] (use-state nil)] + (use-callback (fn [error] + (set-error (fn [] (throw error)))) + [set-error]))) + +(defn use-promise + "React hook which resolves a promise and handles errors." + [p] + (let [handle-error (use-error-handler) + !state (use-state nil)] + (use-effect (fn [] + (-> p + (.then #(reset! !state %)) + (.catch handle-error))) + #js []) + @!state)) + +(defn ^js use-d3-require [package] + (let [p (react/useMemo #(apply d3-require/require + (cond-> package + (string? package) + list)) + #js[(str package)])] + (use-promise p))) From a15773c19c7ced6286aad73ff63c781ecf01ac67 Mon Sep 17 00:00:00 2001 From: Martin Kavalar Date: Thu, 10 Nov 2022 13:06:03 +0100 Subject: [PATCH 08/13] Fix replace mishap --- src/nextjournal/clerk/render.cljs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/nextjournal/clerk/render.cljs b/src/nextjournal/clerk/render.cljs index 5af4f4361..f29207afb 100644 --- a/src/nextjournal/clerk/render.cljs +++ b/src/nextjournal/clerk/render.cljs @@ -340,8 +340,8 @@ [:span.group.hover:bg-indigo-100.rounded-sm.hover:shadow.cursor-pointer {:class (when multi-expand? "bg-indigo-100 shadow ") :on-click (partial toggle-expanded !expanded-at path) - :on-mohooks/use-enter #(swap! !expanded-at assoc :hover-path path) - :on-mohooks/use-leave #(swap! !expanded-at dissoc :hover-path)} + :on-mouse-enter #(swap! !expanded-at assoc :hover-path path) + :on-mouse-leave #(swap! !expanded-at dissoc :hover-path)} [:span.text-slate-400.group-hover:text-indigo-700 {:class (when multi-expand? "text-indigo-700 ")} [triangle expanded?]] From 2e36a79a19ae97807282ed1e584654e3292867fe Mon Sep 17 00:00:00 2001 From: Andrea Amantini Date: Thu, 10 Nov 2022 14:17:52 +0100 Subject: [PATCH 09/13] Reuse Codemirror views --- resources/viewer-js-hash | 2 +- src/nextjournal/clerk/render/code.cljs | 23 +++++++++++++++-------- src/nextjournal/clerk/render/hooks.cljs | 1 + 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/resources/viewer-js-hash b/resources/viewer-js-hash index 4d2ed8543..1c82d86ac 100644 --- a/resources/viewer-js-hash +++ b/resources/viewer-js-hash @@ -1 +1 @@ -RNrtY4qLtvYCdE6D8SvR2GVSUfV \ No newline at end of file +3XDUeU8Vv9MWA4mLWncrPefKectM \ No newline at end of file diff --git a/src/nextjournal/clerk/render/code.cljs b/src/nextjournal/clerk/render/code.cljs index bf75c3e40..6ddbd537a 100644 --- a/src/nextjournal/clerk/render/code.cljs +++ b/src/nextjournal/clerk/render/code.cljs @@ -68,14 +68,21 @@ (.. EditorView -editable (of false)) cm-theme]) -(defn cm-view [doc parent] - (EditorView. (j/obj :state (.create EditorState (j/obj :doc doc :extensions cm-extensions)) - :parent parent))) +(defn cm-state [doc] (.create EditorState (j/obj :doc doc :extensions cm-extensions))) +(defn cm-view [state parent] (EditorView. (j/obj :state state :parent parent))) (defn render-code [value] - (let [ref (hooks/use-ref nil)] + (let [!container (hooks/use-ref nil) + !view (hooks/use-ref nil) + !state (hooks/use-ref (cm-state value))] (hooks/use-effect (fn [] - (let [^js editor-view (cm-view value @ref)] - #(.destroy editor-view))) [value]) - [:div {:ref ref}])) - + (js/console.log :create-view value) + (let [^js view (cm-view @!state @!container)] + (reset! !view view) + #(do (js/console.log :destroy-view) (.destroy view)))) []) + (hooks/use-effect (fn [] + (js/console.log :create-state value) + (let [state (cm-state value)] + (reset! !state state) + (when @!view (.setState @!view state)))) [value]) + [:div {:ref !container}])) diff --git a/src/nextjournal/clerk/render/hooks.cljs b/src/nextjournal/clerk/render/hooks.cljs index 4bd3a1120..969bb4036 100644 --- a/src/nextjournal/clerk/render/hooks.cljs +++ b/src/nextjournal/clerk/render/hooks.cljs @@ -1,6 +1,7 @@ (ns nextjournal.clerk.render.hooks (:require ["d3-require" :as d3-require] ["react" :as react] + [reagent.ratom] ["use-sync-external-store/shim" :refer [useSyncExternalStore]])) ;; a type for wrapping react/useState to support reset! and swap! From 5bef9c1fb8b9ae4f4c3b758bc54d7d13dceee415 Mon Sep 17 00:00:00 2001 From: Andrea Amantini Date: Thu, 10 Nov 2022 14:36:31 +0100 Subject: [PATCH 10/13] Make hooks IReset behaviour comply with clojure --- resources/viewer-js-hash | 2 +- src/nextjournal/clerk/render/code.cljs | 12 +++++------- src/nextjournal/clerk/render/hooks.cljs | 7 +++++-- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/resources/viewer-js-hash b/resources/viewer-js-hash index 1c82d86ac..eabc3763c 100644 --- a/resources/viewer-js-hash +++ b/resources/viewer-js-hash @@ -1 +1 @@ -3XDUeU8Vv9MWA4mLWncrPefKectM \ No newline at end of file +3dqwF2EpC5AdE5foJEN3X9wmwqRq \ No newline at end of file diff --git a/src/nextjournal/clerk/render/code.cljs b/src/nextjournal/clerk/render/code.cljs index 6ddbd537a..13f0ccffa 100644 --- a/src/nextjournal/clerk/render/code.cljs +++ b/src/nextjournal/clerk/render/code.cljs @@ -77,12 +77,10 @@ !state (hooks/use-ref (cm-state value))] (hooks/use-effect (fn [] (js/console.log :create-view value) - (let [^js view (cm-view @!state @!container)] - (reset! !view view) - #(do (js/console.log :destroy-view) (.destroy view)))) []) + (let [^js view (reset! !view (cm-view @!state @!container))] + #(do (js/console.log :destroy-view value) (.destroy view)))) []) (hooks/use-effect (fn [] - (js/console.log :create-state value) - (let [state (cm-state value)] - (reset! !state state) - (when @!view (.setState @!view state)))) [value]) + (js/console.log :reset-state value) + (cond->> (reset! !state (cm-state value)) + @!view (.setState @!view))) [value]) [:div {:ref !container}])) diff --git a/src/nextjournal/clerk/render/hooks.cljs b/src/nextjournal/clerk/render/hooks.cljs index 969bb4036..79b65c4dc 100644 --- a/src/nextjournal/clerk/render/hooks.cljs +++ b/src/nextjournal/clerk/render/hooks.cljs @@ -15,7 +15,8 @@ (-reset! [^js this new-value] ;; `constantly` here ensures that if we reset state to a fn, ;; it is stored as-is and not applied to prev value. - ((aget st 1) (constantly new-value))) + ((aget st 1) (constantly new-value)) + new-value) ISwap (-swap! [this f] ((aget st 1) f)) (-swap! [this f a] ((aget st 1) #(f % a))) @@ -54,7 +55,9 @@ IDeref (-deref [^js this] (.-current this)) IReset - (-reset! [^js this new-value] (set! (.-current this) new-value)) + (-reset! [^js this new-value] + (set! (.-current this) new-value) + new-value) ISwap (-swap! ([o f] (reset! o (f o))) From e55171215e0bc4f84b5c2bc02109d04639b82d8d Mon Sep 17 00:00:00 2001 From: Martin Kavalar Date: Thu, 10 Nov 2022 15:00:31 +0100 Subject: [PATCH 11/13] Cleanup & naming pass --- resources/viewer-js-hash | 2 +- src/nextjournal/clerk/render/code.cljs | 35 ++++++++++++-------------- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/resources/viewer-js-hash b/resources/viewer-js-hash index eabc3763c..31e21434d 100644 --- a/resources/viewer-js-hash +++ b/resources/viewer-js-hash @@ -1 +1 @@ -3dqwF2EpC5AdE5foJEN3X9wmwqRq \ No newline at end of file +4FZT2RWG4WSCNxYdzEF1zozjrga9 \ No newline at end of file diff --git a/src/nextjournal/clerk/render/code.cljs b/src/nextjournal/clerk/render/code.cljs index 13f0ccffa..0b355a8bd 100644 --- a/src/nextjournal/clerk/render/code.cljs +++ b/src/nextjournal/clerk/render/code.cljs @@ -8,7 +8,7 @@ [nextjournal.clojure-mode :as clojure-mode])) ;; code viewer -(def cm-theme +(def theme (.theme EditorView (j/lit {"&.cm-focused" {:outline "none"} ".cm-line" {:padding "0" @@ -30,7 +30,7 @@ ".cm-tooltip > ul > li:first-child" {:border-top-left-radius "3px" :border-top-right-radius "3px"}}))) -(def cm-highlight-style +(def highlight-style (.define HighlightStyle (clj->js [{:tag (.-meta tags) :class "cmt-meta"} {:tag (.-link tags) :class "cmt-link"} @@ -62,25 +62,22 @@ {:tag (.-comment tags) :class "cmt-comment"} {:tag (.-invalid tags) :class "cmt-invalid"}]))) -(def cm-extensions +(def extensions #js [clojure-mode/default-extensions - (syntaxHighlighting cm-highlight-style) + (syntaxHighlighting highlight-style) (.. EditorView -editable (of false)) - cm-theme]) + theme]) -(defn cm-state [doc] (.create EditorState (j/obj :doc doc :extensions cm-extensions))) -(defn cm-view [state parent] (EditorView. (j/obj :state state :parent parent))) +(defn make-state [doc] + (.create EditorState (j/obj :doc doc :extensions extensions))) + +(defn make-view [state parent] + (EditorView. (j/obj :state state :parent parent))) (defn render-code [value] - (let [!container (hooks/use-ref nil) - !view (hooks/use-ref nil) - !state (hooks/use-ref (cm-state value))] - (hooks/use-effect (fn [] - (js/console.log :create-view value) - (let [^js view (reset! !view (cm-view @!state @!container))] - #(do (js/console.log :destroy-view value) (.destroy view)))) []) - (hooks/use-effect (fn [] - (js/console.log :reset-state value) - (cond->> (reset! !state (cm-state value)) - @!view (.setState @!view))) [value]) - [:div {:ref !container}])) + (let [!container-el (hooks/use-ref nil) + !view (hooks/use-ref nil)] + (hooks/use-effect (fn [] (let [^js view (reset! !view (make-view (make-state value) @!container-el))] + #(.destroy view)))) + (hooks/use-effect (fn [] (.setState @!view (make-state value))) [value]) + [:div {:ref !container-el}])) From 69877bbc954127f5224b8c203a4d754df9e83804 Mon Sep 17 00:00:00 2001 From: Martin Kavalar Date: Thu, 10 Nov 2022 15:10:38 +0100 Subject: [PATCH 12/13] Expose hooks to sci env --- notebooks/viewers/errors.clj | 13 +++++++------ resources/viewer-js-hash | 2 +- src/nextjournal/clerk/sci_env.cljs | 5 +++++ 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/notebooks/viewers/errors.clj b/notebooks/viewers/errors.clj index 0119e73cd..e774b6bcc 100644 --- a/notebooks/viewers/errors.clj +++ b/notebooks/viewers/errors.clj @@ -21,11 +21,12 @@ 42) -(clerk/with-viewer {:render-fn '(fn [x] (let [handle-error (nextjournal.clerk.render/use-error-handler) - ref(nextjournal.clerk.render/use-callback (fn [_] - (-> (js/Promise. (fn [resolve reject] - (js/setTimeout (fn [] (resolve 1)), 200))) - (.then (fn [x] (throw (js/Error "async error 💥")))) - (.catch handle-error))))] +(clerk/with-viewer {:render-fn '(fn [x] (let [handle-error (nextjournal.clerk.render.hooks/use-error-handler) + ref (nextjournal.clerk.render.hooks/use-callback + (fn [_] + (-> (js/Promise. (fn [resolve reject] + (js/setTimeout (fn [] (resolve 1)), 200))) + (.then (fn [x] (throw (js/Error "async error 💥")))) + (.catch handle-error))))] [:h3 {:ref ref} (str x)]))} 42) diff --git a/resources/viewer-js-hash b/resources/viewer-js-hash index 31e21434d..31c3c04de 100644 --- a/resources/viewer-js-hash +++ b/resources/viewer-js-hash @@ -1 +1 @@ -4FZT2RWG4WSCNxYdzEF1zozjrga9 \ No newline at end of file +2XYszeQZsvhrowWVf4KHtDREv8b6 \ No newline at end of file diff --git a/src/nextjournal/clerk/sci_env.cljs b/src/nextjournal/clerk/sci_env.cljs index 4de2917f0..b93b2a6aa 100644 --- a/src/nextjournal/clerk/sci_env.cljs +++ b/src/nextjournal/clerk/sci_env.cljs @@ -6,6 +6,7 @@ [goog.object] [nextjournal.clerk.parser] [nextjournal.clerk.render :as render] + [nextjournal.clerk.render.hooks :as hooks] [nextjournal.clerk.trim-image] [nextjournal.clerk.viewer :as viewer] [nextjournal.view.context :as view-context] @@ -68,6 +69,9 @@ (def parser-namespace (sci/copy-ns nextjournal.clerk.parser (sci/create-ns 'nextjournal.clerk.parser))) +(def hooks-namespace + (sci/copy-ns nextjournal.clerk.render.hooks (sci/create-ns 'nextjournal.clerk.render.hooks))) + (defonce !sci-ctx (atom (sci/init {:async? true :disable-arity-checks true @@ -79,6 +83,7 @@ 'v 'nextjournal.clerk.viewer 'p 'nextjournal.clerk.parser} :namespaces (merge {'nextjournal.clerk.render render-namespace + 'nextjournal.clerk.render.hooks hooks-namespace 'nextjournal.clerk.viewer viewer-namespace 'nextjournal.clerk.parser parser-namespace 'clojure.core {'swap! nextjournal.clerk.render/clerk-swap!}} From 5ba34d21645888e6cfcbf13310c554b6628112d2 Mon Sep 17 00:00:00 2001 From: Andrea Amantini Date: Thu, 10 Nov 2022 15:35:43 +0100 Subject: [PATCH 13/13] Use layout-effect to render code cells synchronously --- notebooks/{ => nextjournal/clerk}/atom.clj | 0 resources/viewer-js-hash | 2 +- src/nextjournal/clerk/render/code.cljs | 4 ++-- src/nextjournal/clerk/render/hooks.cljs | 6 ++++++ 4 files changed, 9 insertions(+), 3 deletions(-) rename notebooks/{ => nextjournal/clerk}/atom.clj (100%) diff --git a/notebooks/atom.clj b/notebooks/nextjournal/clerk/atom.clj similarity index 100% rename from notebooks/atom.clj rename to notebooks/nextjournal/clerk/atom.clj diff --git a/resources/viewer-js-hash b/resources/viewer-js-hash index 31c3c04de..bf44f144c 100644 --- a/resources/viewer-js-hash +++ b/resources/viewer-js-hash @@ -1 +1 @@ -2XYszeQZsvhrowWVf4KHtDREv8b6 \ No newline at end of file +HyZRVkbHxkzXU5g5XFyw5QDwF9x \ No newline at end of file diff --git a/src/nextjournal/clerk/render/code.cljs b/src/nextjournal/clerk/render/code.cljs index 0b355a8bd..84ad1e1bd 100644 --- a/src/nextjournal/clerk/render/code.cljs +++ b/src/nextjournal/clerk/render/code.cljs @@ -77,7 +77,7 @@ (defn render-code [value] (let [!container-el (hooks/use-ref nil) !view (hooks/use-ref nil)] - (hooks/use-effect (fn [] (let [^js view (reset! !view (make-view (make-state value) @!container-el))] - #(.destroy view)))) + (hooks/use-layout-effect (fn [] (let [^js view (reset! !view (make-view (make-state value) @!container-el))] + #(.destroy view)))) (hooks/use-effect (fn [] (.setState @!view (make-state value))) [value]) [:div {:ref !container-el}])) diff --git a/src/nextjournal/clerk/render/hooks.cljs b/src/nextjournal/clerk/render/hooks.cljs index 79b65c4dc..d3d383605 100644 --- a/src/nextjournal/clerk/render/hooks.cljs +++ b/src/nextjournal/clerk/render/hooks.cljs @@ -45,6 +45,12 @@ ([f] (react/useEffect (wrap-effect f) #js[])) ([f deps] (react/useEffect (wrap-effect f) (as-array deps)))) +(defn use-layout-effect + "React hook: useLayoutEffect. Defaults to an empty `deps` array. + Wraps `f` to return js/undefined for any non-function value." + ([f] (react/useLayoutEffect (wrap-effect f) #js[])) + ([f deps] (react/useLayoutEffect (wrap-effect f) (as-array deps)))) + (defn use-state "React hook: useState. Can be used like react/useState but also behaves like an atom." [init]