├── src └── reagent_dev_tools │ ├── styles.cljs │ ├── styles.clj │ ├── preload.clj │ ├── context.cljs │ ├── preload.cljs │ ├── state.cljs │ ├── utils.cljs │ ├── styles.css │ ├── state_tree.cljs │ └── core.cljs ├── example-src ├── .gitignore ├── html │ └── public │ │ └── index.html └── cljs │ └── example │ └── main.cljs ├── .gitignore ├── package.json ├── shadow-cljs.edn ├── project.clj ├── CHANGELOG.md └── README.md /src/reagent_dev_tools/styles.cljs: -------------------------------------------------------------------------------- 1 | (ns reagent-dev-tools.styles 2 | (:require-macros reagent-dev-tools.styles)) 3 | -------------------------------------------------------------------------------- /example-src/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | .DS_Store 9 | /.nrepl-port 10 | /.repl 11 | /.lein* 12 | -------------------------------------------------------------------------------- /src/reagent_dev_tools/styles.clj: -------------------------------------------------------------------------------- 1 | (ns reagent-dev-tools.styles 2 | (:require [clojure.java.io :as io])) 3 | 4 | (defmacro main-css [] 5 | (slurp (io/resource "reagent_dev_tools/styles.css"))) 6 | -------------------------------------------------------------------------------- /src/reagent_dev_tools/preload.clj: -------------------------------------------------------------------------------- 1 | (ns reagent-dev-tools.preload 2 | (:require [cljs.env])) 3 | 4 | (defmacro read-config [] 5 | (when cljs.env/*compiler* 6 | (get-in @cljs.env/*compiler* [:options :external-config :reagent-dev-tools]))) 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | .DS_Store 9 | /.nrepl-port 10 | /.repl 11 | /.lein* 12 | 13 | node_modules/ 14 | .shadow-cljs/ 15 | 16 | figwheel_server.log 17 | .rebel_readline_history 18 | -------------------------------------------------------------------------------- /src/reagent_dev_tools/context.cljs: -------------------------------------------------------------------------------- 1 | (ns reagent-dev-tools.context 2 | (:require ["react" :as react])) 3 | 4 | (defonce panel-context (react/createContext nil)) 5 | 6 | (def panel-context-provider (.-Provider panel-context)) 7 | (def panel-context-consumer (.-Consumer panel-context)) 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reagent-dev-tools", 3 | "scripts": { 4 | "start": "npx shadow-cljs watch client" 5 | }, 6 | "dependencies": { 7 | "react": "^19.2.0", 8 | "react-dom": "^19.2.0" 9 | }, 10 | "private": true, 11 | "devDependencies": { 12 | "shadow-cljs": "^2.28.10" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | {:source-paths ["src" "example-src/cljs" "example-src/html"] 2 | :dev-http {8090 {:roots ["target/shadow-cljs/client/public" "classpath:public"]}} 3 | :builds {:client {:target :browser 4 | :output-dir "target/shadow-cljs/client/public" 5 | :asset-path "/js" 6 | :modules {:main {:entries [example.main]}}}} 7 | :dependencies [[reagent "2.0.0"] 8 | [frankiesardo/linked "1.3.0"]]} 9 | -------------------------------------------------------------------------------- /example-src/html/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/reagent_dev_tools/preload.cljs: -------------------------------------------------------------------------------- 1 | (ns reagent-dev-tools.preload 2 | (:require-macros [reagent-dev-tools.preload :refer [read-config]]) 3 | (:require [reagent-dev-tools.core :as core])) 4 | 5 | ;; Wait until JS is loaded, because config will refer to namespaces 6 | ;; that will be loaded after this preload ns. 7 | ;; Those namesapces will be available after document is "interactive". 8 | ;; Start right-away, if document is already loaded. 9 | 10 | (defn start [] 11 | (let [opts (read-config)] 12 | (core/start! opts))) 13 | 14 | (if (#{"interactive" "complete"} (.. js/document -readyState)) 15 | (start) 16 | (.addEventListener js/document "DOMContentLoaded" #(start))) 17 | -------------------------------------------------------------------------------- /src/reagent_dev_tools/state.cljs: -------------------------------------------------------------------------------- 1 | (ns reagent-dev-tools.state 2 | (:require [reagent.core :as r] 3 | [cljs.reader :as reader])) 4 | 5 | (def storage-key (str :reagent-dev-tools.core/state)) 6 | 7 | (defonce dev-state 8 | (r/atom (merge {:height 300 9 | :width 300} 10 | (try 11 | (reader/read-string (.getItem js/localStorage storage-key)) 12 | (catch :default _ 13 | nil))))) 14 | 15 | ;; Save the state (open, height, active panel) to local storage 16 | (add-watch dev-state :local-storage 17 | (fn [_ _ _old v] 18 | (.setItem js/localStorage storage-key (pr-str (dissoc v :mouse))))) 19 | -------------------------------------------------------------------------------- /src/reagent_dev_tools/utils.cljs: -------------------------------------------------------------------------------- 1 | (ns reagent-dev-tools.utils 2 | (:require [reagent.core :as r] 3 | [clojure.set :as set] 4 | [clojure.string :as string])) 5 | 6 | ;; From https://github.com/metosin/komponentit/blob/master/src/cljs/komponentit/mixins.cljs 7 | 8 | (defn ->event-type [k] 9 | (-> k name (string/replace #"^on-" "") (string/replace #"-" ""))) 10 | 11 | (defn- update-listeners [el listeners props this] 12 | (swap! listeners 13 | (fn [listeners] 14 | (let [current-event-types (set (keys listeners)) 15 | new-event-types (set (keys props))] 16 | (as-> listeners $ 17 | (reduce (fn [listeners k] 18 | (let [f (fn [e] 19 | ;; Need to retrieve latest callback in case the props have been updated 20 | (when-let [f (get (r/props this) k)] 21 | (f e)))] 22 | (.addEventListener el (->event-type k) f) 23 | (assoc listeners k f))) 24 | $ (set/difference new-event-types current-event-types)) 25 | (reduce (fn [listeners k] 26 | (.removeEventListener el (->event-type k) (get listeners k)) 27 | (dissoc listeners k)) 28 | $ (set/difference current-event-types new-event-types))))))) 29 | 30 | (defn window-event-listener 31 | [_] 32 | (let [listeners (atom nil)] 33 | (r/create-class 34 | {:display-name "komponentit.mixins.window_event_listener_class" 35 | :component-did-mount (fn [this] (update-listeners js/window listeners (r/props this) this)) 36 | :component-did-update (fn [this] (update-listeners js/window listeners (r/props this) this)) 37 | :component-will-unmount (fn [this] (update-listeners js/window listeners {} this)) 38 | :reagent-render 39 | (fn [_props child] 40 | child)}))) 41 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject metosin/reagent-dev-tools "1.1.0" 2 | ;; :url "" 3 | :license {:name "Eclipse Public License"} 4 | :description "Reagent dev tools" 5 | 6 | :dependencies [] 7 | 8 | :plugins [[lein-cljsbuild "1.1.8"] 9 | [lein-doo "0.1.11"] 10 | [lein-codox "0.10.8"] 11 | [lein-figwheel "0.5.20"]] 12 | 13 | :source-paths ["src"] 14 | 15 | :profiles {:dev {:dependencies [[org.clojure/clojure "1.12.3"] 16 | [org.clojure/clojurescript "1.11.132"] 17 | [figwheel "0.5.20"] 18 | [figwheel-sidecar "0.5.20"] 19 | 20 | [reagent "2.0.0"] 21 | 22 | [frankiesardo/linked "1.3.0" :scope "test"] 23 | [cljsjs/react "18.3.1-1"] 24 | [cljsjs/react-dom "18.3.1-1"]] 25 | :source-paths ["example-src/cljs"] 26 | :resource-paths ["example-src/html" "target/cljsbuild/client"]}} 27 | 28 | :clean-targets ^{:protect false} [:target-path :compile-path "out"] 29 | 30 | :repl-options {:init (require '[figwheel-sidecar.repl-api :refer :all])} 31 | 32 | :figwheel {:http-server-root "public" ;; assumes "resources" 33 | :css-dirs ["site/public/css"] 34 | :repl true 35 | :nrepl-port 27397} 36 | 37 | :cljsbuild 38 | {:builds 39 | [{:id "client" 40 | :source-paths ["example-src/cljs"] 41 | :watch-paths ["src" "example-src/cljs"] 42 | :figwheel true 43 | :compiler {:parallel-build true 44 | :optimizations :none 45 | :main "example.main" 46 | :output-dir "target/cljsbuild/client/public/out" 47 | :output-to "target/cljsbuild/client/public/main.js" 48 | :npm-deps false 49 | :asset-path "out" 50 | :checked-arrays :warn 51 | :infer-externs true}}]}) 52 | 53 | -------------------------------------------------------------------------------- /example-src/cljs/example/main.cljs: -------------------------------------------------------------------------------- 1 | (ns example.main 2 | (:require ["react" :as react] 3 | [reagent.core :as r] 4 | [reagent.dom :as rdom] 5 | [reagent.dom.client :as rdomc] 6 | [reagent-dev-tools.core :as dev-tools] 7 | [reagent-dev-tools.state :as rdt-state] 8 | [linked.map :as lm] 9 | [linked.core :as l])) 10 | 11 | (def state 12 | (r/atom {:hello "world" 13 | :fn-test (fn foo-name []) 14 | :anon-fn #(constantly nil) 15 | :number 1337 16 | :kw :namespace/keyword 17 | :nil nil 18 | :foo {:bar "bar" 19 | :items [{:id "1" :name "a"} 20 | {:id "2" :name "b"}] 21 | "items" {:string-key 1} 22 | nil {:nil-key 2} 23 | [1 2 :foo] {:vector-key 3} 24 | (js/Symbol "symbol") {:js-symbol-key 4}} 25 | :linked-map (l/map :foo 1 26 | :bar 2)})) 27 | 28 | (defonce update-numbers 29 | (js/setInterval (fn [] 30 | (swap! state assoc :number (rand-int 9001)) 31 | (swap! state assoc-in [:foo :bar] (str "bar" (rand-int 9001)))) 32 | 500)) 33 | 34 | (def users (r/atom [{:id "1" :name "a"} 35 | {:id "2" :name "b"}])) 36 | 37 | (dev-tools/register-collection-info-handler! 38 | lm/LinkedMap 39 | #(dev-tools/collection-info-handler "LinkedMap" "{LinkedMap, " (count %) " keys}")) 40 | 41 | (defn example-panel 42 | [text] 43 | [:div 44 | [:h1 "Hello " text] 45 | [:p "React version: " react/version]]) 46 | 47 | (def dev-panels 48 | [{:key :users 49 | :label "Users" 50 | :view [dev-tools/state-tree 51 | {:label "example.main/users" 52 | :ratom users}]} 53 | {:key :dev-tools 54 | :label "Dev tools state" 55 | :view [dev-tools/state-tree 56 | {:ratom rdt-state/dev-state}]} 57 | nil 58 | {:key :example1 59 | :label "Example panel" 60 | :view [example-panel "foo"]}]) 61 | 62 | (defn main [] 63 | [:h1 "hello"] ) 64 | 65 | (defonce el (.getElementById js/document "app")) 66 | (defonce root (delay (rdomc/create-root el))) 67 | 68 | (defn restart! [] 69 | (if (exists? react-dom/render) 70 | (rdom/render [main] el) 71 | (rdomc/render @root [main])) 72 | (dev-tools/start! {:margin-element js/document.body 73 | :state-atom state 74 | :state-atom-name "App state" 75 | :panels dev-panels})) 76 | 77 | (restart!) 78 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.1.0 (2025-10-31) 2 | 3 | [compare](https://github.com/metosin/reagent-dev-tools/compare/1.0.3...1.1.0) 4 | 5 | - Support for either React 19 or 18 using Reagent 2.0 6 | - `reagent.dom` or `reagent.dom.client` is chosen based on if `react-dom/render` 7 | is available. React 17 is not uspported because `react-dom/client` has to 8 | exist. 9 | 10 | # 1.0.3 (2024-04-19) 11 | 12 | [compare](https://github.com/metosin/reagent-dev-tools/compare/1.0.2...1.0.3) 13 | 14 | - Fix `toggle-btn` option default 15 | 16 | # 1.0.2 (2024-04-19) 17 | 18 | [compare](https://github.com/metosin/reagent-dev-tools/compare/1.0.1...1.0.2) 19 | 20 | - Add `toggle-btn` option to replace the open button component 21 | 22 | # 1.0.1 (2023-11-22) 23 | 24 | [compare](https://github.com/metosin/reagent-dev-tools/compare/1.0.0...1.0.1) 25 | 26 | - Remove `font: revert` rule and set `font-weight` for certain elements so that 27 | Tailwind reset doesn't break it. 28 | This allows users to use their styles (like MUI) within their own panels. 29 | 30 | # 1.0.0 (2022-04-22) 31 | 32 | [compare](https://github.com/metosin/reagent-dev-tools/compare/0.4.2...1.0.0) 33 | 34 | - **New**: 35 | - If `:state-atom` value isn't given, no default panel is added if `:panels` 36 | is defined. If neither is provided, help text is displayed. 37 | - New `:panels` option 38 | - Panels are now defined as a vector, so they keep their order. 39 | - Panels are appended to default panels. 40 | - State-tree component and functions is now accessible through the core namespace: 41 | - `reagent-dev-tools.core/state-tree` 42 | - `reagent-dev-tools.core/register-collection-info-handler!` 43 | - `reagent-dev-tools.core/collection-info-handler` 44 | - **Breaking**: 45 | - Removed `register-state-atom` 46 | - Use `:panels` with additional `state-tree` components instead. 47 | - Removed `:panels-fn` option 48 | - Use new `:panels` list, which is just a vector instead of map returning map. 49 | - Requires Reagent 1.0.0+ 50 | - Add `max-width: 100vw` to prevent vertical panel being wider than screen width 51 | - Navigation bar panel list now wraps to multiple lines if it doesn't fit on one line 52 | 53 | # 0.4.2 (2022-03-18) 54 | 55 | [compare](https://github.com/metosin/reagent-dev-tools/compare/0.4.1...0.4.2) 56 | 57 | - Reset `font: inherit` rule from tailwind 58 | - Reset `line-height` 59 | 60 | # 0.4.1 (2022-03-18) 61 | 62 | [compare](https://github.com/metosin/reagent-dev-tools/compare/0.4.0...0.4.1) 63 | 64 | - Ensure text `color`, `font-style` and `font-weight` are reset inside the panel 65 | 66 | # 0.4.0 (2021-01-12) 67 | 68 | [compare](https://github.com/metosin/reagent-dev-tools/compare/0.3.1...0.4.0) 69 | 70 | - Add way to control collection description text for custom types, 71 | like Linked: 72 | 73 | ``` 74 | (state-tree/register-collection-info-handler 75 | lm/LinkedMap 76 | #(state-tree/collection-info-handler "LinkedMap" "{LinkedMap, " (count %) " keys}")) 77 | ``` 78 | 79 | # 0.3.1 (2020-12-30) 80 | 81 | [compare](https://github.com/metosin/reagent-dev-tools/compare/0.3.0...0.3.1) 82 | 83 | - Added toggle collection to state atoms 84 | - Add `:state-atom-name` option to customize name for state atom added using `start!` 85 | - Ensure `nil`, vectors and other things as map keys in the state tree 86 | are rendered. 87 | 88 | # 0.3.0 (2020-12-07) 89 | 90 | [compare](https://github.com/metosin/reagent-dev-tools/compare/0.2.1...0.3.0) 91 | 92 | - Add option to toggle panel placement between bottom and right 93 | - Add `:margin-element` option to automatically set margin-bottom/right on some 94 | element, so that panel doesn't go over the application content 95 | - Store open paths on state tree to local storage 96 | 97 | # 0.2.1 (2020-03-06) 98 | 99 | - Use `reagent.dom/render` instead of `reagent.core/render` to prepare for 100 | next Reagent releases 101 | 102 | # 0.2.0 (2018-01-25) 103 | 104 | - Made the panel resizeable 105 | - Save the state (open, height, active panel) on `localStorage` 106 | - Added `start!` function for easier configuration 107 | - Added some colors to the state tree 108 | - Add collection type name to state tree 109 | - Type can be clicked to open/close collection items 110 | - Fixes 111 | 112 | # 0.1.0 (2015-11-29) 113 | 114 | - Initial release 115 | -------------------------------------------------------------------------------- /src/reagent_dev_tools/styles.css: -------------------------------------------------------------------------------- 1 | .reagent-dev-tools__panel, .reagent-dev-tools__toggle-btn { 2 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, Roboto, 'Helvetica Neue', sans-serif; 3 | font-size: 16px; 4 | line-height: 1.2; 5 | font-style: normal; 6 | font-weight: normal; 7 | color: #000; 8 | } 9 | 10 | .reagent-dev-tools__panel { 11 | z-index: 2000; 12 | position: fixed; 13 | background: #fff; 14 | text-align: left; 15 | display: flex; 16 | 17 | /* Prevent vertical panel being wider than the screen width */ 18 | max-width: 100vw; 19 | } 20 | 21 | .reagent-dev-tools__table { 22 | border-collapse: collapse; 23 | border-spacing: 0; 24 | } 25 | 26 | .reagent-dev-tools__nav { 27 | padding: 0 0 0 10px; 28 | margin: 0; 29 | border-bottom: 1px solid #ccc; 30 | display: flex; 31 | flex-direction: row; 32 | align-items: center; 33 | background: #eee; 34 | flex: 0 0 auto; 35 | } 36 | 37 | .reagent-dev-tools__nav-panels { 38 | display: flex; 39 | flex-direction: row; 40 | flex: 1 1 0%; 41 | flex-wrap: wrap; 42 | } 43 | 44 | .reagent-dev-tools__nav-li { 45 | margin-top: 5px; 46 | margin-bottom: -1px; 47 | margin-right: 10px; 48 | } 49 | 50 | .reagent-dev-tools__nav-li-a { 51 | display: inline-block; 52 | padding: 10px; 53 | cursor: pointer; 54 | background: #eee; 55 | color: #666; 56 | border: 1px solid #ccc; 57 | border-bottom-color: #ccc; 58 | } 59 | 60 | .reagent-dev-tools__nav-li-a:hover { 61 | background: #BDF; 62 | } 63 | 64 | .reagent-dev-tools__nav-li-a--active { 65 | background: #fff; 66 | color: #333; 67 | border-bottom-color: #fff; 68 | } 69 | 70 | .reagent-dev-tools__spacer { 71 | flex: 1 0 0%; 72 | } 73 | 74 | .reagent-dev-tools__nav-li-a--option-button { 75 | /* Nav text are 20 + 20 + 2 = 42px high. Icons are 24px, so padding should fill rest. */ 76 | padding: 8px 10px; 77 | margin-top: 5px; 78 | margin-left: 1px; 79 | margin-bottom: -1px; 80 | background: #fff; 81 | } 82 | 83 | .reagent-dev-tools__nav-li-a--close-button { 84 | padding: 8px 10px; 85 | margin-top: 5px; 86 | margin-left: 10px; 87 | margin-bottom: -1px; 88 | background: #fff; 89 | } 90 | 91 | .reagent-dev-tools__td { 92 | padding: 0; 93 | } 94 | 95 | .reagent-dev-tools__pre { 96 | font-family: 'SFMono-Regular', 'Ubuntu Mono', Consolas, 'DejaVu Sans Mono', Menlo, monospace; 97 | display: inline; 98 | background: none; 99 | border: 0; 100 | padding: 0; 101 | } 102 | 103 | .reagent-dev-tools__ul { 104 | padding-left: 1em; 105 | } 106 | 107 | .reagent-dev-tools__li { 108 | padding-left: 1em; 109 | text-indent: -1em; 110 | list-style: none; 111 | } 112 | 113 | .reagent-dev-tools__li-toggle { 114 | padding: 2px 6px; 115 | } 116 | 117 | .reagent-dev-tools__li-toggle--active { 118 | cursor: pointer; 119 | } 120 | 121 | .reagent-dev-tools__li-toggle--active:hover { 122 | background: #BDF; 123 | } 124 | 125 | .reagent-dev-tools__li-toggle-icon { 126 | display: inline-block; 127 | } 128 | 129 | .reagent-dev-tools__pull-right { 130 | float: right; 131 | } 132 | 133 | /* nav-li-a-style */ 134 | .reagent-dev-tools__toggle-btn { 135 | position: fixed; 136 | bottom: 0; 137 | right: 0; 138 | z-index: 2000; 139 | } 140 | 141 | .reagent-dev-tools__panel-content { 142 | padding: 10px; 143 | overflow-y: auto; 144 | overflow-x: auto; 145 | flex: 1 0 0%; 146 | } 147 | 148 | .reagent-dev-tools__sizer { 149 | background: #888; 150 | flex: 0 0 auto; 151 | } 152 | 153 | .reagent-dev-tools__sizer:hover { 154 | background: #79D 155 | } 156 | 157 | .reagent-dev-tools__keyword { 158 | color: #6f42c1; 159 | } 160 | 161 | .reagent-dev-tools__string { 162 | color: #44A344; 163 | } 164 | 165 | .reagent-dev-tools__number { 166 | color: #005cc5; 167 | } 168 | 169 | .reagent-dev-tools__nil { 170 | color: #888; 171 | font-style: italic; 172 | } 173 | 174 | .reagent-dev-tools__strong { 175 | font-weight: bold; 176 | } 177 | 178 | .reagent-dev-tools__bottom-icon, .reagent-dev-tools__right-icon { 179 | box-sizing: border-box; 180 | width: 24px; 181 | height: 24px; 182 | border: 1px solid #888; 183 | } 184 | 185 | .reagent-dev-tools__bottom-icon { 186 | border-bottom-width: 12px; 187 | } 188 | 189 | .reagent-dev-tools__right-icon { 190 | border-right-width: 12px; 191 | } 192 | 193 | .reagent-dev-tools__close-icon { 194 | width: 24px; 195 | height: 24px; 196 | position: relative; 197 | } 198 | 199 | .reagent-dev-tools__close-icon::before, .reagent-dev-tools__close-icon::after { 200 | position: absolute; 201 | left: 12px; 202 | content: ' '; 203 | height: 24px; 204 | width: 2px; 205 | background-color: #888; 206 | } 207 | 208 | .reagent-dev-tools__close-icon::before { 209 | transform: rotate(45deg); 210 | } 211 | 212 | .reagent-dev-tools__close-icon::after { 213 | transform: rotate(-45deg); 214 | } 215 | -------------------------------------------------------------------------------- /src/reagent_dev_tools/state_tree.cljs: -------------------------------------------------------------------------------- 1 | (ns reagent-dev-tools.state-tree 2 | (:require ["react" :as react] 3 | [reagent.core :as r] 4 | [reagent-dev-tools.state :as state] 5 | [reagent-dev-tools.context :as ctx])) 6 | 7 | (defn- toggle [v ks open?] 8 | (if (or (not (get-in v ks)) 9 | open?) 10 | (assoc-in v ks {}) 11 | (assoc-in v ks nil))) 12 | 13 | (defn type->class [v] 14 | (cond 15 | (keyword? v) "reagent-dev-tools__keyword" 16 | (string? v) "reagent-dev-tools__string" 17 | (number? v) "reagent-dev-tools__number" 18 | (nil? v) "reagent-dev-tools__nil")) 19 | 20 | (defn collection-info-handler 21 | "- type-name is for tooltip 22 | - start is the opening parenthesis and maybe type-name for custom types 23 | - count 24 | - end is description for count name, e.g. 'items' and the end parenthesis" 25 | [type-name start count end] 26 | [:span 27 | {:title type-name} 28 | (str start 29 | count 30 | end)]) 31 | 32 | (def ^:private collection-info 33 | (atom {PersistentHashMap #(collection-info-handler "PersistentHashMap" "{" (count %) " keys}") 34 | PersistentArrayMap #(collection-info-handler "PersistentArrayMap" "{" (count %) " keys}") 35 | PersistentHashSet #(collection-info-handler "PersistentHashSet" "#{" (count %) " items}") 36 | PersistentVector #(collection-info-handler "PersistentVector" "[" (count %) " items]") 37 | Cons #(collection-info-handler "Cons" "(" (count %) " items)") 38 | List #(collection-info-handler "List" "(" (count %) " items)") 39 | EmptyList #(collection-info-handler "EmptyList" "(" (count %) " items)")})) 40 | 41 | (defn register-collection-info-handler [type handler] 42 | (swap! collection-info assoc type handler)) 43 | 44 | (defn collection-desc [v] 45 | (let [t (type v)] 46 | (if-let [f (get @collection-info t)] 47 | (f v) 48 | ;; Basic handling for custom types implementing IMap etc, but without 49 | ;; type-name tooltip other info. 50 | (cond 51 | (map? v) (str "{" (count v) " keys}") 52 | (vector? v) (str "[" (count v) " items]") 53 | (set? v) (str "#{" (count v) " items}") 54 | (list? v) (str "(" (count v) " items)") 55 | :else (str "unknown type: " (type->str t)))))) 56 | 57 | (defn- toggle-item [open open-fn v ks] 58 | [:span.reagent-dev-tools__li-toggle.reagent-dev-tools__li-toggle--active.reagent-dev-tools__pre 59 | {:on-click (fn [_] 60 | ;; if one is closed, open all 61 | ;; else close all 62 | (let [open-all? (some nil? (vals open))] 63 | (doseq [[k _] (if (map? v) 64 | v 65 | (zipmap (range) v))] 66 | (open-fn (conj ks k) open-all?)))) 67 | :on-mouse-down (fn [e] 68 | ;; Disable text select after double click 69 | (when (> (.-detail e) 1) 70 | (.preventDefault e)))} 71 | (collection-desc v)]) 72 | 73 | (defn- tree [open open-fn v ks] 74 | (if (coll? v) 75 | [:ul.reagent-dev-tools__ul 76 | (for [[k v] (if (map? v) 77 | v 78 | (zipmap (range) v)) 79 | :let [open (get open k) 80 | ks (conj ks k)]] 81 | [:li.reagent-dev-tools__li 82 | {:key (pr-str k)} 83 | [:span.reagent-dev-tools__li-toggle 84 | {:on-click #(open-fn ks false) 85 | :title "Toggle this collection" 86 | :class (when (coll? v) 87 | "reagent-dev-tools__li-toggle--active")} 88 | (when (coll? v) 89 | [:span.reagent-dev-tools__li-toggle-icon 90 | (if open "-" "+")]) 91 | [:span.reagent-dev-tools__strong 92 | {:class (type->class k)} 93 | (pr-str k)] 94 | 95 | " "] 96 | (when (coll? v) 97 | [toggle-item open open-fn v ks]) 98 | (when (or (not (coll? v)) open) 99 | [tree open open-fn v ks])])] 100 | 101 | [:pre.reagent-dev-tools__pre 102 | {:class (type->class v)} 103 | (pr-str v)])) 104 | 105 | (defn state-tree-panel' [{:keys [label ratom]} panel-opts] 106 | [:div 107 | (let [k (:key panel-opts) 108 | open (get-in @state/dev-state [:state-tree k :open]) 109 | open-fn (fn [ks open?] (swap! state/dev-state update-in [:state-tree k :open] toggle ks open?)) 110 | ratom-v @ratom] 111 | [:div 112 | [:span.reagent-dev-tools__strong 113 | (or label 114 | (:label panel-opts)) 115 | [toggle-item open open-fn ratom-v []]] 116 | [tree 117 | open 118 | open-fn 119 | ratom-v 120 | []]])]) 121 | 122 | (defn state-tree-panel [opts] 123 | [:> ctx/panel-context-consumer 124 | (fn [v] 125 | (r/as-element [state-tree-panel' opts v]))]) 126 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reagent-dev-tools 2 | 3 | [![Clojars Project](http://clojars.org/metosin/reagent-dev-tools/latest-version.svg)](http://clojars.org/metosin/reagent-dev-tools) 4 | 5 | ## Features 6 | 7 | - Compatibility 8 | - Reagent 2 and 1.3 9 | - React 19 and 18 10 | - Display state tree for Reagent atoms 11 | - Styles embedded for easy use 12 | - Simple toggleable development tool panel which can be extended with own tabs 13 | 14 | ## Configuration 15 | 16 | ### Options 17 | 18 | - `:el` (optional) The element to render the dev-tool into. If the property is given, 19 | but is nil, dev tool is not enabled. If not given, new div is created and used. 20 | - `:margin-element` (optional) Element where to set margin-bottom/right if the panel is open. 21 | This is helpful so that the dev tool isn't displayed over the application content. 22 | - `:state-atom` This options adds default `state-tree` panel displaying tree for the given RAtom. 23 | - `:state-atom-name` (optional) Overrides the name for default `state-tree` panel. 24 | - `:panels` List of panel maps to display. This is appended to the default panels, if you 25 | don't want to include default panels, leave out :state-atom option and define all panels here. 26 | - `:toggle-btn` (optional) Reagent component to render the open button. Takes `open-fn` as parameter. 27 | Rendered as functional component so the component can also use hooks. 28 | 29 | #### Panel options: 30 | - `:key` (Required) React key 31 | - `:label` (Required) Label for tab bar 32 | - `:view` (Required) Reagent Hiccup form to display the panel content 33 | 34 | #### Built-in panel component options: 35 | 36 | - `reagent-dev-tools.core/state-tree` 37 | - `:ratom` (Required) The RAtom to display 38 | - `:label` (Optional) Label to display for atom root node, will default to panel :label. 39 | 40 | ### 1. :preload namespace 41 | 42 | To enable, add `reagent-dev-tools.preload` to your `:compiler-options` `:preloads`. 43 | This will ensure that Dev tool is only included in the output JS for 44 | development builds. 45 | 46 | To configure tool in this setup, you can use `:external-config :reagent-dev-tools` option: 47 | 48 | ```edn 49 | :external-config {:reagent-dev-tools {:state-atom example.main/state 50 | :panels example.main/panels}}}} 51 | ``` 52 | 53 | ### 2. Start manually based on compile time options 54 | 55 | You could use `goog.DEBUG` or other Closure define options to call the start function 56 | on your application code: 57 | 58 | ```cljs 59 | (ns example.app 60 | (:require [reagent-dev-tools.core :as dev-tools] 61 | re-frame.db)) 62 | 63 | ;; FIXME: Is typehint required nowadays? 64 | (when ^boolean goog.DEBUG 65 | (dev-tools/start! {:state-atom re-frame.db/app-db})) 66 | ``` 67 | 68 | Note: as you are requiring the namespace always, it is possible that 69 | Google Closure is not able to remove all reagent-dev-tools code during DCE. 70 | 71 | ### 3. Start dynamically and use separate module 72 | 73 | You can use [JS Modules](https://clojurescript.org/reference/javascript-module-support) 74 | or [Shadow CLJS modules](https://shadow-cljs.github.io/docs/UsersGuide.html#_modules) 75 | so split the reagent-dev-tools code to a separate module you can load dynamically. 76 | You could for example load some options from your backend, look at the 77 | browser location or `localStorage`. 78 | 79 | ```cljs 80 | (ns example.app 81 | (:require [shadow.loader :as loader])) 82 | 83 | ;; Load reagent-dev-tools on localhost, using specific hash url 84 | ;; or if enabled manually from JS console. 85 | (defn enable-dev-tools [] 86 | (case (.. js/document -location -hash) 87 | "#enable-dev-tool" (do 88 | (.setItem js/localStorage "reagent-dev-tools" "1") 89 | (loader/load "devtools")) 90 | "#disable-dev-tool" (.removeItem js/localStorage "reagent-dev-tools") 91 | nil) 92 | 93 | (when (or (= "localhost" (.. js/document -location -hostname)) 94 | (.getItem js/localStorage "reagent-dev-tools")) 95 | (loader/load "devtools"))) 96 | 97 | (defn ^:export enable-dev-tools! [] 98 | (.setItem js/localStorage "reagent-dev-tools" "1") 99 | (enable-dev-tools)) 100 | 101 | (.addEventListener js/window "load" (fn [] (enable-dev-tools))) 102 | ``` 103 | 104 | ```cljs 105 | (ns example.dev 106 | (:require [reagent-dev-tools.core :as dev-tools] 107 | re-frame.db)) 108 | 109 | (dev-tools/start! {:state-atom re-frame.db/app-db}) 110 | ``` 111 | 112 | ### 4. Using the components as part of the application 113 | 114 | Reagent component `reagent-dev-tools.core/dev-tool` can also be used directly 115 | as part of Reagent applications. 116 | 117 | ## Extending 118 | 119 | ### Custom type handling 120 | 121 | Custom handlers can be registered to better display if custom collection type 122 | is displayed, so it can be differentiated from regular vectors and maps: 123 | 124 | ``` 125 | (dev-tools/register-collection-info-handler 126 | lm/LinkedMap 127 | #(dev-tools/collection-info-handler "LinkedMap" "{LinkedMap, " (count %) " keys}")) 128 | ``` 129 | 130 | ### Panels 131 | 132 | The panels components can access the panel options map through React context 133 | defined in `reagent-dev-tools/context`. Check state-tree implementation. 134 | 135 | ## License 136 | 137 | Copyright © 2015-2025 [Metosin Oy](http://www.metosin.fi) 138 | 139 | Distributed under the Eclipse Public License, the same as Clojure. 140 | -------------------------------------------------------------------------------- /src/reagent_dev_tools/core.cljs: -------------------------------------------------------------------------------- 1 | (ns reagent-dev-tools.core 2 | (:require ["react-dom" :as react-dom] 3 | [reagent.core :as r] 4 | [reagent.dom :as rdom] 5 | [reagent.dom.client :as rdomc] 6 | [reagent-dev-tools.styles :as s] 7 | [reagent-dev-tools.state-tree :as state-tree] 8 | [reagent-dev-tools.state :as state] 9 | [reagent-dev-tools.utils :refer [window-event-listener]] 10 | [reagent-dev-tools.context :as ctx])) 11 | 12 | (def element-id (str ::dev-panel)) 13 | 14 | (def state-tree state-tree/state-tree-panel) 15 | (def collection-info-handler state-tree/collection-info-handler) 16 | (def register-collection-info-handler! state-tree/register-collection-info-handler) 17 | 18 | (defn create-default-panels [options] 19 | (if (:state-atom options) 20 | [{:key ::default 21 | :label (:state-atom-name options "State") 22 | :view [state-tree 23 | {:k :state-atom 24 | :ratom (:state-atom options)}]}] 25 | (if (nil? (:panels options)) 26 | [{:key ::default 27 | :label (:state-atom-name options "State") 28 | :view [:div [:p "Configure either `:state-atom` or `:panels`."]]}] 29 | []))) 30 | 31 | (defn default-toggle-btn [open-fn] 32 | [:button.reagent-dev-tools__nav-li-a.reagent-dev-tools__toggle-btn 33 | {:on-click open-fn} 34 | "dev"]) 35 | 36 | (defn dev-tool 37 | #_:clj-kondo/ignore 38 | [{:keys [panels] 39 | :as options}] 40 | (let [mouse-state (r/atom nil)] 41 | (fn [{:keys [panels margin-element toggle-btn]}] 42 | (let [{:keys [open? place width height]} @state/dev-state 43 | 44 | toggle-btn (or toggle-btn default-toggle-btn) 45 | panels (keep identity panels) 46 | id->panel (into {} (map (juxt :key identity) panels))] 47 | (when margin-element 48 | (set! (.. margin-element -style -marginRight) (when (and open? (= :right place)) 49 | (str width "px"))) 50 | 51 | (set! (.. margin-element -style -marginBottom) (when (and open? (= :bottom place)) 52 | (str height "px")))) 53 | [:<> 54 | [:style (s/main-css)] 55 | (if open? 56 | (let [current-k (:current @state/dev-state ::default) 57 | current-panel (or (get id->panel current-k) 58 | (::default id->panel))] 59 | [window-event-listener 60 | {:on-mouse-move (when @mouse-state 61 | (fn [e] 62 | (.preventDefault e) 63 | (swap! state/dev-state 64 | (fn [v] 65 | (case place 66 | :right (assoc v :width (-> (- (.-innerWidth js/window) (.-clientX e)) 67 | (max 250) 68 | (min 1000))) 69 | ;; Bottom 70 | (assoc v :height (-> (- (.-innerHeight js/window) (.-clientY e)) 71 | (max 50) 72 | (min 1000)))))))) 73 | :on-mouse-up (when @mouse-state 74 | (fn [_e] 75 | (reset! mouse-state nil)))} 76 | [:div.reagent-dev-tools__panel 77 | {:style (case place 78 | :right {:width (str width "px") 79 | :top 0 80 | :right 0 81 | :height "100%" 82 | :flex-direction "row"} 83 | ;; bottom 84 | {:height (str height "px") 85 | :width "100%" 86 | :bottom 0 87 | :left 0 88 | :flex-direction "column"})} 89 | [:div.reagent-dev-tools__sizer 90 | {:style (case place 91 | :right {:width "5px" 92 | :cursor "ew-resize"} 93 | ;; bottom 94 | {:height "5px" 95 | :cursor "ns-resize"}) 96 | :on-mouse-down (fn [e] 97 | (reset! mouse-state true) 98 | (.preventDefault e))}] 99 | [:div 100 | {:style {:display "flex" 101 | :flex-direction "column" 102 | :flex "1 0 auto" 103 | :width "100%" 104 | :height "100%"}} 105 | [:div.reagent-dev-tools__nav 106 | [:div.reagent-dev-tools__nav-panels 107 | (for [panel panels] 108 | [:div.reagent-dev-tools__nav-li 109 | {:key (name (:key panel))} 110 | [:a.reagent-dev-tools__nav-li-a 111 | {:class (when (keyword-identical? current-k (:key panel)) "reagent-dev-tools__nav-li-a--active") 112 | :on-click #(swap! state/dev-state assoc :current (:key panel))} 113 | (:label panel)]])] 114 | 115 | ;; Just diplay the button to toggle to the other state. 116 | (if (= :right place) 117 | [:button.reagent-dev-tools__nav-li-a.reagent-dev-tools__nav-li-a--option-button 118 | {:on-click #(swap! state/dev-state assoc :place :bottom)} 119 | [:div.reagent-dev-tools__bottom-icon]] 120 | [:button.reagent-dev-tools__nav-li-a.reagent-dev-tools__nav-li-a--option-button 121 | {:on-click #(swap! state/dev-state assoc :place :right)} 122 | [:div.reagent-dev-tools__right-icon]]) 123 | 124 | [:button.reagent-dev-tools__nav-li-a.reagent-dev-tools__nav-li-a--close-button 125 | {:on-click #(swap! state/dev-state assoc :open? false)} 126 | [:div.reagent-dev-tools__close-icon]]] 127 | 128 | ;; Allow the panel component to access panel-options through React context 129 | ;; E.g. to access the panel :key or :label 130 | [:div.reagent-dev-tools__panel-content 131 | [:r> ctx/panel-context-provider 132 | #js {:value current-panel} 133 | (:view current-panel)]]]]]) 134 | [:f> toggle-btn 135 | (fn [_] 136 | (swap! state/dev-state assoc :open? true) 137 | nil)])])))) 138 | 139 | (def ^:private panels-fn-warning 140 | (delay (js/console.warn "Reagent dev tools option `:panels-fn` is deprecated. Use `:panels` instead."))) 141 | 142 | ;; Create one root per given element 143 | ;; Avoids warnings on re-calling start! on the same element 144 | (defonce create-root 145 | (memoize (fn [el] 146 | (rdomc/create-root el)))) 147 | 148 | ;; NOTE: sync the option changes to README. 149 | (defn start! 150 | "Start Reagent dev tool. 151 | 152 | Options: 153 | 154 | - `:el` (optional) The element to render the dev-tool into. If the property is given, 155 | but is nil, dev tool is not enabled. If not given, new div is created and used. 156 | - `:margin-element` (optional) Element where to set margin-bottom/right if the panel is open. 157 | This is helpful so that the dev tool isn't displayed over the application content. 158 | - `:state-atom` This options adds default `state-tree` panel displaying tree for the given RAtom. 159 | - `:state-atom-name` (optional) Overrides the name for default `state-tree` panel. 160 | - `:panels` List of panel maps to display. This is appended to the default panels, if you 161 | don't want to include default panels, leave out :state-atom option and define all panels here. 162 | - `:toggle-btn` (optional) Reagent component to render the open button. Takes `open-fn` as parameter. 163 | Rendered as functional component so the component can also use hooks. 164 | 165 | Panel options: 166 | - `:key` (Required) React key 167 | - `:label` (Required) Label for tab bar 168 | - `:view` (Required) Reagent Hiccup form to display the panel content 169 | 170 | Built-in panel component options: 171 | 172 | - `reagent-dev-tools.core/state-tree` 173 | - `:ratom` (Required) The RAtom to display 174 | - `:label` (Optional) Label to display for atom root node, will default to panel :label." 175 | [opts] 176 | (when (:panels-fn opts) 177 | @panels-fn-warning) 178 | 179 | (doseq [panel (:panels opts) 180 | :when (some? panel)] 181 | (assert (:key panel) "Panel :key is required") 182 | (assert (vector? (:view panel)) "Panel :view is required and must an vector")) 183 | 184 | (when-let [el (if (contains? opts :el) 185 | (:el opts) 186 | (or (.getElementById js/document element-id) 187 | (let [el (.createElement js/document "div")] 188 | (set! (.-id el) element-id) 189 | (.appendChild (.-body js/document) el) 190 | el)))] 191 | 192 | (let [comp [dev-tool {:margin-element (:margin-element opts) 193 | :toggle-btn (:toggle-btn opts) 194 | :panels (into (create-default-panels opts) 195 | (:panels opts))}]] 196 | (if (exists? react-dom/render) 197 | (rdom/render comp el) 198 | (rdomc/render (create-root el) comp))))) 199 | --------------------------------------------------------------------------------