├── .gitignore ├── README.md ├── doc └── principles.md ├── project.clj ├── resources └── public │ ├── css │ └── style.css │ └── index.html ├── src └── proact │ ├── core.cljs │ ├── examples │ └── todo.cljc │ ├── html.cljc │ ├── html_util.cljc │ ├── render │ ├── browser.cljs │ ├── dom.cljc │ ├── expand.cljc │ ├── loop.cljc │ └── state.cljc │ ├── util.cljc │ └── widgets │ ├── controls.cljc │ ├── layout.cljc │ └── tools.cljc ├── test └── proact │ └── render │ └── expand_test.clj └── todo.js /.gitignore: -------------------------------------------------------------------------------- 1 | /resources/public/js/compiled/** 2 | figwheel_server.log 3 | pom.xml 4 | *jar 5 | /lib/ 6 | /classes/ 7 | /out/ 8 | /target/ 9 | .lein-deps-sum 10 | .lein-repl-history 11 | .lein-plugins/ 12 | .repl 13 | .nrepl-port 14 | /checkouts/ 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Proact 2 | 3 | [](https://gitter.im/brandonbloom/proact?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | Yet another UI Framework (in ClojureScript). 6 | 7 | ## Brief 8 | 9 | I was building something like this before React.js came out. When I got scooped 10 | on the render-loop / virtual-dom design, I went on to other side projects. 11 | However, the time has come to revisit this and my abandoned design notes 12 | featuring details that I feel no other project has gotten right quite yet. The 13 | time is now because I've also got an application that I want to build and none 14 | of the existing platforms exactly meet my criteria. 15 | 16 | Watch this space or Twitter for updates, including motivation and design notes. 17 | 18 | I've written some initial thoughts on [design principles][3]. 19 | 20 | ## Usage 21 | 22 | Don't. At least not yet. 23 | 24 | But if you want to play with it, there's a [partial TodoMVC implementation][1] 25 | and [some driver code][2] for it. Do the `lein figwheel` thing. 26 | 27 | ## Contributing 28 | 29 | One thing you could contribute is a better name. Any ideas? :-) 30 | 31 | Since this project is still in the incubation stage, I'm mostly interested in 32 | contributions in the form of thoughtful discussions. Once the design principles 33 | are validated and the API stablizes, I'll be interested in a wider variety of 34 | contributions. That said, don't hesitate to reach out! 35 | 36 | ## License 37 | 38 | Copyright © 2015 Brandon Bloom 39 | 40 | Distributed under the Eclipse Public License either version 1.0 or (at 41 | your option) any later version. 42 | 43 | 44 | [1]: ./src/proact/examples/todo.cljc 45 | [2]: ./src/proact/core.cljs 46 | [3]: ./doc/principles.md 47 | -------------------------------------------------------------------------------- /doc/principles.md: -------------------------------------------------------------------------------- 1 | # Design Principles 2 | 3 | This document is a work-in-progress. New principles will be added and existing 4 | ones refined. These principles will be used to motivate specific designs. 5 | 6 | 7 | ## Gain Leverage Through Tooling 8 | 9 | Outside of pixel art and retro polygon modeling, graphic designers do not 10 | loving place each and every individual pixel; modelers do not tweak every 11 | vertex by hand. And even if they did, they sure as hell wouldn't type in RGB 12 | hex codes or enter precise coordinate values. No, they'd use a graphical tool 13 | with direct manipulation. 14 | 15 | When faced with a tradeoff between convenient syntax for programmers and 16 | uniformity that can be leveraged by tools, err in favor of the latter. When 17 | the pain of inconvenient syntax and tiresome symbolic tweaking becomes too 18 | great to bear, prefer automation over indirection. 19 | 20 | 21 | ## Default To Concrete 22 | 23 | > "This idea that there is generality in the 24 | > specific is of far-reaching importance." 25 | > -- Douglas R. Hofstadter 26 | 27 | As functional ideals gain traction in graphical user interface programming, 28 | lambda begins to drown out all other mechanisms of abstraction. Functions 29 | acheive abstraction through a priori parameterization and renders the 30 | abstracted expression inert in the absence of those parameters. Components 31 | described by template functions are flexible, but they are unforgiving. If 32 | supplied incomplete or incorrect parameters, the template will fail. 33 | 34 | By contrast, a specific working component can be instanced and modified to 35 | yield a new working component. Each incremental modification preserves the 36 | integrety of the composition, such that development of components can proceed 37 | independently of their consumers. This property is critical for collaboration 38 | with designers, effective tooling, and exploratory implementation. Therefore, 39 | we prefer to abstract via prototypes, rather than templates. 40 | 41 | 42 | ## Write Once, Tune Everywhere 43 | 44 | Even if "Write once, run everywhere" worked, it wouldn't be desirable for user 45 | interfaces. Consistency with other applications on a platform is more 46 | important than consistency of a single application between platforms. 47 | 48 | As an alternative, "Learn once, write anywhere" leverages perspective and 49 | tooling between platforms, but abandons the challenge of leveraging code reuse. 50 | 51 | Instead, aim for the ideal of "Write once, tune anywhere", where you can reuse 52 | an adjustable range of production code and assets between platforms. To achieve 53 | this, the tooling must both expose and isolate host platform semantics. 54 | Abstractions that span platforms must be provided out of the box and they 55 | should achieve platform specialization in the same manner as components 56 | are abstracted for reuse within a particular platform. 57 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject proact "0.1.0-SNAPSHOT" 2 | :description "FIXME: write this!" 3 | :url "http://example.com/FIXME" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | 7 | :dependencies [[org.clojure/clojure "1.7.0"] 8 | [org.clojure/clojurescript "0.0-3269"] 9 | [org.clojure/core.rrb-vector "0.0.11"] 10 | [org.clojure/core.match "0.3.0-alpha4"] 11 | [bbloom.vdom "0.0.2"]] 12 | 13 | :plugins [[lein-cljsbuild "1.0.5"] 14 | [lein-figwheel "0.3.3"]] 15 | 16 | :jvm-opts ^:replace ["-Xms1g" "-Xmx2g" "-server"] 17 | 18 | :source-paths ["src"] 19 | 20 | :clean-targets ^{:protect false} ["resources/public/js/compiled" "target"] 21 | 22 | :cljsbuild { 23 | :builds [{:id "dev" 24 | :source-paths ["src"] 25 | 26 | :figwheel { :on-jsload "proact.core/on-js-reload" } 27 | 28 | :compiler {:main proact.core 29 | :asset-path "js/compiled/out" 30 | :output-to "resources/public/js/compiled/proact.js" 31 | :output-dir "resources/public/js/compiled/out" 32 | :source-maps true 33 | :source-map-timestamp true}} 34 | {:id "min" 35 | :source-paths ["src"] 36 | :compiler {:output-to "resources/public/js/compiled/proact.js" 37 | :main proact.core 38 | :optimizations :advanced 39 | :elide-asserts true 40 | :pretty-print false}}]} 41 | 42 | :figwheel { 43 | ;; :http-server-root "public" ;; default and assumes "resources" 44 | ;; :server-port 3449 ;; default 45 | :css-dirs ["resources/public/css"] ;; watch and update CSS 46 | 47 | ;; Start an nREPL server into the running figwheel process 48 | ;; :nrepl-port 7888 49 | 50 | ;; Server Ring Handler (optional) 51 | ;; if you want to embed a ring handler into the figwheel http-kit 52 | ;; server, this is for simple ring servers, if this 53 | ;; doesn't work for you just run your own server :) 54 | ;; :ring-handler hello_world.server/handler 55 | 56 | ;; To be able to open files in your editor from the heads up display 57 | ;; you will need to put a script on your path. 58 | ;; that script will have to take a file path and a line number 59 | ;; ie. in ~/bin/myfile-opener 60 | ;; #! /bin/sh 61 | ;; emacsclient -n +$2 $1 62 | ;; 63 | ;; :open-file-command "myfile-opener" 64 | 65 | ;; if you want to disable the REPL 66 | ;; :repl false 67 | 68 | ;; to configure a different figwheel logfile path 69 | ;; :server-logfile "tmp/logs/figwheel-logfile.log" 70 | }) 71 | -------------------------------------------------------------------------------- /resources/public/css/style.css: -------------------------------------------------------------------------------- 1 | 2 | html, 3 | body { 4 | margin: 0; 5 | padding: 0; 6 | } 7 | 8 | button { 9 | margin: 0; 10 | padding: 0; 11 | border: 0; 12 | background: none; 13 | font-size: 100%; 14 | vertical-align: baseline; 15 | font-family: inherit; 16 | font-weight: inherit; 17 | color: inherit; 18 | -webkit-appearance: none; 19 | appearance: none; 20 | -webkit-font-smoothing: antialiased; 21 | -moz-font-smoothing: antialiased; 22 | font-smoothing: antialiased; 23 | } 24 | 25 | body { 26 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 27 | line-height: 1.4em; 28 | background: #f5f5f5; 29 | color: #4d4d4d; 30 | min-width: 230px; 31 | max-width: 550px; 32 | margin: 0 auto; 33 | -webkit-font-smoothing: antialiased; 34 | -moz-font-smoothing: antialiased; 35 | font-smoothing: antialiased; 36 | font-weight: 300; 37 | } 38 | 39 | button, 40 | input[type="checkbox"] { 41 | outline: none; 42 | } 43 | 44 | .hidden { 45 | display: none; 46 | } 47 | 48 | #todoapp { 49 | background: #fff; 50 | margin: 130px 0 40px 0; 51 | position: relative; 52 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 53 | 0 25px 50px 0 rgba(0, 0, 0, 0.1); 54 | } 55 | 56 | #todoapp input::-webkit-input-placeholder { 57 | font-style: italic; 58 | font-weight: 300; 59 | color: #e6e6e6; 60 | } 61 | 62 | #todoapp input::-moz-placeholder { 63 | font-style: italic; 64 | font-weight: 300; 65 | color: #e6e6e6; 66 | } 67 | 68 | #todoapp input::input-placeholder { 69 | font-style: italic; 70 | font-weight: 300; 71 | color: #e6e6e6; 72 | } 73 | 74 | #todoapp h1 { 75 | position: absolute; 76 | top: -155px; 77 | width: 100%; 78 | font-size: 100px; 79 | font-weight: 100; 80 | text-align: center; 81 | color: rgba(175, 47, 47, 0.15); 82 | -webkit-text-rendering: optimizeLegibility; 83 | -moz-text-rendering: optimizeLegibility; 84 | text-rendering: optimizeLegibility; 85 | } 86 | 87 | #new-todo, 88 | .edit { 89 | position: relative; 90 | margin: 0; 91 | width: 100%; 92 | font-size: 24px; 93 | font-family: inherit; 94 | font-weight: inherit; 95 | line-height: 1.4em; 96 | border: 0; 97 | outline: none; 98 | color: inherit; 99 | padding: 6px; 100 | border: 1px solid #999; 101 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 102 | box-sizing: border-box; 103 | -webkit-font-smoothing: antialiased; 104 | -moz-font-smoothing: antialiased; 105 | font-smoothing: antialiased; 106 | } 107 | 108 | #new-todo { 109 | padding: 16px 16px 16px 60px; 110 | border: none; 111 | background: rgba(0, 0, 0, 0.003); 112 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); 113 | } 114 | 115 | #main { 116 | position: relative; 117 | z-index: 2; 118 | border-top: 1px solid #e6e6e6; 119 | } 120 | 121 | label[for='toggle-all'] { 122 | display: none; 123 | } 124 | 125 | #toggle-all { 126 | position: absolute; 127 | top: -55px; 128 | left: -12px; 129 | width: 60px; 130 | height: 34px; 131 | text-align: center; 132 | border: none; /* Mobile Safari */ 133 | } 134 | 135 | #toggle-all:before { 136 | content: '\276F'; 137 | font-size: 22px; 138 | color: #e6e6e6; 139 | padding: 10px 27px 10px 27px; 140 | } 141 | 142 | #toggle-all:checked:before { 143 | color: #737373; 144 | } 145 | 146 | #todo-list { 147 | margin: 0; 148 | padding: 0; 149 | list-style: none; 150 | } 151 | 152 | #todo-list li { 153 | position: relative; 154 | font-size: 24px; 155 | border-bottom: 1px solid #ededed; 156 | } 157 | 158 | #todo-list li:last-child { 159 | border-bottom: none; 160 | } 161 | 162 | #todo-list li.editing { 163 | border-bottom: none; 164 | padding: 0; 165 | } 166 | 167 | #todo-list li.editing .edit { 168 | display: block; 169 | width: 506px; 170 | padding: 13px 17px 12px 17px; 171 | margin: 0 0 0 43px; 172 | } 173 | 174 | #todo-list li.editing .view { 175 | display: none; 176 | } 177 | 178 | #todo-list li .toggle { 179 | text-align: center; 180 | width: 40px; 181 | /* auto, since non-WebKit browsers doesn't support input styling */ 182 | height: auto; 183 | position: absolute; 184 | top: 0; 185 | bottom: 0; 186 | margin: auto 0; 187 | border: none; /* Mobile Safari */ 188 | -webkit-appearance: none; 189 | appearance: none; 190 | } 191 | 192 | #todo-list li .toggle:after { 193 | content: url('data:image/svg+xml;utf8,'); 194 | } 195 | 196 | #todo-list li .toggle:checked:after { 197 | content: url('data:image/svg+xml;utf8,'); 198 | } 199 | 200 | #todo-list li label { 201 | white-space: pre; 202 | word-break: break-word; 203 | padding: 15px 60px 15px 15px; 204 | margin-left: 45px; 205 | display: block; 206 | line-height: 1.2; 207 | transition: color 0.4s; 208 | } 209 | 210 | #todo-list li.completed label { 211 | color: #d9d9d9; 212 | text-decoration: line-through; 213 | } 214 | 215 | #todo-list li .destroy { 216 | display: none; 217 | position: absolute; 218 | top: 0; 219 | right: 10px; 220 | bottom: 0; 221 | width: 40px; 222 | height: 40px; 223 | margin: auto 0; 224 | font-size: 30px; 225 | color: #cc9a9a; 226 | margin-bottom: 11px; 227 | transition: color 0.2s ease-out; 228 | } 229 | 230 | #todo-list li .destroy:hover { 231 | color: #af5b5e; 232 | } 233 | 234 | #todo-list li .destroy:after { 235 | content: '\d7'; 236 | } 237 | 238 | #todo-list li:hover .destroy { 239 | display: block; 240 | } 241 | 242 | #todo-list li .edit { 243 | display: none; 244 | } 245 | 246 | #todo-list li.editing:last-child { 247 | margin-bottom: -1px; 248 | } 249 | 250 | #footer { 251 | color: #777; 252 | padding: 10px 15px; 253 | height: 20px; 254 | text-align: center; 255 | border-top: 1px solid #e6e6e6; 256 | } 257 | 258 | #footer:before { 259 | content: ''; 260 | position: absolute; 261 | right: 0; 262 | bottom: 0; 263 | left: 0; 264 | height: 50px; 265 | overflow: hidden; 266 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 267 | 0 8px 0 -3px #f6f6f6, 268 | 0 9px 1px -3px rgba(0, 0, 0, 0.2), 269 | 0 16px 0 -6px #f6f6f6, 270 | 0 17px 2px -6px rgba(0, 0, 0, 0.2); 271 | } 272 | 273 | #todo-count { 274 | float: left; 275 | text-align: left; 276 | } 277 | 278 | #todo-count strong { 279 | font-weight: 300; 280 | } 281 | 282 | #filters { 283 | margin: 0; 284 | padding: 0; 285 | list-style: none; 286 | position: absolute; 287 | right: 0; 288 | left: 0; 289 | } 290 | 291 | #filters li { 292 | display: inline; 293 | } 294 | 295 | #filters li a { 296 | color: inherit; 297 | margin: 3px; 298 | padding: 3px 7px; 299 | text-decoration: none; 300 | border: 1px solid transparent; 301 | border-radius: 3px; 302 | } 303 | 304 | #filters li a.selected, 305 | #filters li a:hover { 306 | border-color: rgba(175, 47, 47, 0.1); 307 | } 308 | 309 | #filters li a.selected { 310 | border-color: rgba(175, 47, 47, 0.2); 311 | } 312 | 313 | #clear-completed, 314 | html #clear-completed:active { 315 | float: right; 316 | position: relative; 317 | line-height: 20px; 318 | text-decoration: none; 319 | cursor: pointer; 320 | position: relative; 321 | } 322 | 323 | #clear-completed:hover { 324 | text-decoration: underline; 325 | } 326 | 327 | #info { 328 | margin: 65px auto 0; 329 | color: #bfbfbf; 330 | font-size: 10px; 331 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); 332 | text-align: center; 333 | } 334 | 335 | #info p { 336 | line-height: 1; 337 | } 338 | 339 | #info a { 340 | color: inherit; 341 | text-decoration: none; 342 | font-weight: 400; 343 | } 344 | 345 | #info a:hover { 346 | text-decoration: underline; 347 | } 348 | 349 | /* 350 | Hack to remove background from Mobile Safari. 351 | Can't use it globally since it destroys checkboxes in Firefox 352 | */ 353 | @media screen and (-webkit-min-device-pixel-ratio:0) { 354 | #toggle-all, 355 | #todo-list li .toggle { 356 | background: none; 357 | } 358 | 359 | #todo-list li .toggle { 360 | height: 40px; 361 | } 362 | 363 | #toggle-all { 364 | -webkit-transform: rotate(90deg); 365 | transform: rotate(90deg); 366 | -webkit-appearance: none; 367 | appearance: none; 368 | } 369 | } 370 | 371 | @media (max-width: 430px) { 372 | #footer { 373 | height: 50px; 374 | } 375 | 376 | #filters { 377 | bottom: 10px; 378 | } 379 | } 380 | -------------------------------------------------------------------------------- /resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/proact/core.cljs: -------------------------------------------------------------------------------- 1 | (ns ^:figwheel-always proact.core 2 | (:require [proact.examples.todo :as todo] 3 | [proact.widgets.tools :as tools] 4 | [proact.render.loop :as loop] 5 | [proact.render.browser :as browser] 6 | [goog.events :as gevents] 7 | [goog.history.EventType :as ghistory]) 8 | (:import [goog History])) 9 | 10 | (enable-console-print!) 11 | 12 | (defonce nav-token (atom nil)) 13 | 14 | (defn render [] 15 | (let [showing (case @nav-token 16 | "/active" :active 17 | "/completed" :completed 18 | :all) 19 | root (assoc todo/app :data {:todos @todo/state :showing showing}) 20 | root (assoc tools/designer :data {:widget root}) 21 | root {:dom/tag "div" 22 | :dom/props browser/delegates 23 | :dom/mount "root" 24 | :children [root]}] 25 | (browser/render root))) 26 | 27 | (defonce watch (loop/add-listener ::watch (fn [& _] (render)))) 28 | 29 | (defn on-navigate [token] 30 | (reset! nav-token token) 31 | (render)) 32 | 33 | (defonce history 34 | (let [h (History.)] 35 | (gevents/listen h ghistory/NAVIGATE #(on-navigate (.-token %))) 36 | (.setEnabled h true) 37 | h)) 38 | 39 | (render) 40 | -------------------------------------------------------------------------------- /src/proact/examples/todo.cljc: -------------------------------------------------------------------------------- 1 | (ns proact.examples.todo 2 | (:require 3 | #?(:clj [clojure.core.match :refer [match]]) 4 | #?(:cljs [cljs.core.match :refer-macros [match]]) 5 | [proact.render.loop :as loop] 6 | [proact.render.state :as state] 7 | [proact.widgets.controls :as ctrl] 8 | #?(:cljs [proact.render.browser :as browser]) 9 | [proact.html :as html] 10 | [proact.html-util :refer [classes link-to]])) 11 | 12 | ;;; Model 13 | 14 | (def mock-todos 15 | [{:id "todo-1" 16 | :text "OMG" 17 | :completed? true} 18 | {:id "todo-2" 19 | :text "it works!" 20 | :completed? false}]) 21 | 22 | (defonce state 23 | (add-watch (atom mock-todos) ::watch (fn [& _] (loop/trigger!)))) 24 | 25 | (defn add-todo [todos text] 26 | (conj todos {:id (str (gensym "todo_")) 27 | :text text 28 | :completed? false})) 29 | 30 | (defn destroy-todo [todos id] 31 | (vec (remove #(= (:id %) id) todos))) 32 | 33 | (defn clear-completed [todos] 34 | (vec (remove :completed? todos))) 35 | 36 | (defn set-completed [todos id value] 37 | (mapv (fn [todo] 38 | (if (= (:id todo) id) 39 | (assoc todo :completed? value) 40 | todo)) 41 | todos)) 42 | 43 | ;;; Event Handlers 44 | 45 | (defn raise! [& args] 46 | (apply swap! state args) 47 | nil) 48 | 49 | (defn app-handler [widget e] 50 | (match [e] 51 | [[:todo/destroy-todo id]] (raise! destroy-todo id) 52 | [[:todo/clear-completed]] (raise! clear-completed) 53 | [[:todo/set-completed id value]] (raise! set-completed id value) 54 | [[:todo/add-todo text]] (raise! add-todo text) 55 | [[:todo/edit id]] (state/put! (:id widget) {:editing id}) 56 | :else e)) 57 | 58 | (defn new-handler [_ e] 59 | (match [e] 60 | [[:key-down 13]] [:todo/add-todo "omg"] ;XXX need text from widget somehow 61 | :else e)) 62 | 63 | (defn todo-handler [widget e] 64 | (case (first e) 65 | :change [:todo/set-completed (-> widget :item :id) (:checked? (second e))] 66 | :double-click [:todo/edit (-> widget :item :id)] 67 | e)) 68 | 69 | ;;; Views 70 | 71 | (def todo-item 72 | ;; onToggle, onDestroy, onEdit, editing, onSave, onCancel 73 | {:handler todo-handler 74 | :template 75 | (fn [{{:keys [completed? editing?] :as todo} :item}] 76 | (html/li {"className" (classes {"completed" completed? 77 | "editing" editing?})} 78 | (html/div {"className" "view"} 79 | (html/input {"className" "toggle" 80 | "type" "checkbox" 81 | ;XXX onChange 82 | "checked" completed?}) 83 | (html/label {} (:text todo)) ;XXX onDoubleClick 84 | (assoc (html/button {"className" "destroy"}) 85 | :prototype ctrl/button 86 | :command [:todo/destroy-todo (:id todo)])) 87 | ;;XXX ref editField 88 | (html/input {"className" "edit" 89 | ;XXX "value" this.state.editText 90 | ;XXX onBlur, onChange, onKeyDown 91 | })))}) 92 | 93 | ;; Fn syntax is more convenient, but loses some benefits of components... 94 | (defn filter-link [showing k href content] 95 | (html/li {} 96 | (html/a {"className" (when (= showing k) "selected") 97 | "href" href} 98 | content))) 99 | 100 | (def todo-footer 101 | {:data {:active 2 :completed 5 :showing :all} 102 | :template 103 | (fn [{{:keys [active completed showing]} :data}] 104 | (html/footer {"id" "footer"} 105 | (html/span {"id" "todo-count"} 106 | (html/strong {} (str active)) 107 | (if (= active 1) " item left" " items left")) 108 | (html/ul {"id" "filters"} 109 | (filter-link showing :all "#/" "All") 110 | (filter-link showing :active "#/active" "Active") 111 | (filter-link showing :completed "#/completed" "Completed")) 112 | (when (pos? completed) 113 | (assoc (html/button {"id" "clear-completed"} 114 | "Clear completed") 115 | :prototype ctrl/button 116 | :command [:todo/clear-completed]))))}) 117 | 118 | (def app 119 | {:data {:todos mock-todos :showing :all} 120 | :state {:editing nil} 121 | :handler app-handler 122 | :template 123 | (fn [{{:keys [todos showing]} :data 124 | {:keys [editing]} :state}] 125 | (let [completed (count (filter :completed? todos)) 126 | active (- (count todos) completed) 127 | footer (assoc todo-footer :data {:active active 128 | :completed completed 129 | :showing showing}) 130 | todos (map #(assoc % :editing? (= (:id %) editing)) todos) 131 | main (when (seq todos) 132 | (html/section {"id" "main"} 133 | (html/input {"id" "toggle-all" 134 | "type" "checkbox" 135 | ;;XXX onChange, checked 136 | "checked" (zero? active)}) 137 | (assoc (html/ul {"id" "todo-list"}) 138 | :item-prototype todo-item 139 | :item-filter (fn [{:keys [completed?]}] 140 | (case showing 141 | :active (not completed?) 142 | :completed completed? 143 | true)) 144 | :items todos))) 145 | input (assoc (html/input {"id" "new-todo" 146 | "placeholder" "What needs to be done?" 147 | "autofocus" true}) 148 | :handler new-handler)] 149 | (html/div {"id" "todoapp"} 150 | (html/header {"id" "header"} 151 | (html/h1 {} "todos") 152 | input) 153 | main 154 | footer)))}) 155 | -------------------------------------------------------------------------------- /src/proact/html.cljc: -------------------------------------------------------------------------------- 1 | (ns proact.html 2 | #?(:cljs (:use-macros [proact.html :only [defelements]]))) 3 | 4 | #?(:clj 5 | (defmacro defelement [name] 6 | `(defn ~name [~'props & ~'children] 7 | {:dom/tag ~(str name) 8 | :dom/props ~'props 9 | :children ~'children}))) 10 | 11 | #?(:clj 12 | (defmacro defelements [& names] 13 | `(do ~@(map #(list `defelement %) names)))) 14 | 15 | (defelements 16 | div span a strong b ul ol li header footer section h1 h2 h3 h4 h5 h6 17 | input label button 18 | ) 19 | -------------------------------------------------------------------------------- /src/proact/html_util.cljc: -------------------------------------------------------------------------------- 1 | (ns proact.html-util 2 | (:require [clojure.string :as str] 3 | [proact.html :as html])) 4 | 5 | (defn classes [m] 6 | (str/join " " (for [[k v] m :when v] k))) 7 | 8 | (defn link-to [href & children] 9 | (apply html/a {"href" href} children)) 10 | -------------------------------------------------------------------------------- /src/proact/render/browser.cljs: -------------------------------------------------------------------------------- 1 | (ns proact.render.browser 2 | (:require [cljs.pprint :refer [pprint]] 3 | [bbloom.vdom.core :as vdom] 4 | [bbloom.vdom.browser :as browser] 5 | [proact.render.dom :refer [tree->vdom]] 6 | [proact.render.expand :refer [expand-all]] 7 | [proact.render.state :as state])) 8 | 9 | ;;XXX Right now this is the expanded *tree*, but should be the graph. 10 | (defonce global (atom nil)) 11 | 12 | (defn render [widget] 13 | (let [expanded (expand-all widget)] 14 | (reset! global expanded) 15 | (-> expanded 16 | tree->vdom 17 | browser/render 18 | ;(select-keys [:trace #_#_ :created :destroyed]) 19 | ;:trace (as-> x (map (comp first second) x)) 20 | ;pprint 21 | ) 22 | nil)) 23 | 24 | (defn path-to [node] 25 | (let [id (browser/identify node)] 26 | ;;XXX Right now this is a linear search, but should be 27 | ;;XXX a hash lookup plus walking parent references. 28 | ((fn rec [path widget] 29 | (let [path (conj path widget)] 30 | (if (= (:id widget) id) 31 | path 32 | (->> widget 33 | :children 34 | (map #(rec path %)) 35 | (filter some?) 36 | first)))) 37 | [] @global))) 38 | 39 | (defn translate [event] 40 | (case (.-type event) 41 | "click" [:click] 42 | "dblclick" [:double-click] 43 | "change" [:change {:checked? (.. event -target -checked)}] 44 | "keydown" [:key-down (.-keyCode event)])) 45 | 46 | (defn route-event [event] 47 | (let [target (.-target event) 48 | path (path-to target) 49 | translated (translate event) 50 | e (reduce (fn [e widget] 51 | (if-let [handler (:handler widget)] 52 | (handler widget e) 53 | e)) 54 | translated 55 | (reverse path))] 56 | (when (not= e translated) 57 | (.stopPropagation event) 58 | (.preventDefault event)) 59 | (when e 60 | (prn 'unhandled e)))) 61 | 62 | (def events [ 63 | "onclick" 64 | "ondblclick" 65 | "onchange" 66 | "onkeydown" 67 | ]) 68 | 69 | (def delegates 70 | (into {} (for [event events] 71 | [event route-event]))) 72 | -------------------------------------------------------------------------------- /src/proact/render/dom.cljc: -------------------------------------------------------------------------------- 1 | (ns proact.render.dom 2 | (:require [bbloom.vdom.core :as vdom])) 3 | 4 | (defn append-child [vdom pid cid] 5 | (let [idx (-> vdom (vdom/node pid) :children count)] 6 | (vdom/insert-child vdom pid idx cid))) 7 | 8 | (defn tree->vdom [widget] 9 | ((fn rec [vdom parent {:keys [id children] 10 | tag :dom/tag, mount :dom/mount 11 | :as widget}] 12 | (when tag 13 | (assert id)) 14 | (let [vdom (cond 15 | (= tag :text) (vdom/create-text vdom id (:text widget)) 16 | tag (-> vdom 17 | (vdom/create-element id tag) 18 | (vdom/set-props id (:dom/props widget))) 19 | :else vdom) 20 | vdom (cond-> vdom 21 | (and tag parent) (append-child parent id) 22 | mount (vdom/mount mount id)) 23 | parent (if tag id parent)] 24 | (reduce #(rec %1 parent %2) 25 | vdom 26 | children))) 27 | vdom/null nil widget)) 28 | 29 | (comment 30 | 31 | (require 'proact.examples.todo) 32 | (require 'proact.render.expand) 33 | (-> 34 | (proact.examples.todo/app {}) 35 | proact.render.expand/expand-all 36 | tree->vdom 37 | fipp.edn/pprint 38 | ) 39 | 40 | ) 41 | -------------------------------------------------------------------------------- /src/proact/render/expand.cljc: -------------------------------------------------------------------------------- 1 | (ns proact.render.expand 2 | (:require [proact.util :refer [flat]] 3 | [proact.render.state :as state])) 4 | 5 | (defn normalize [widget] 6 | (cond 7 | (string? widget) {:dom/tag :text :text widget} ;XXX Use :prototype :text 8 | (map? widget) (update widget :children #(mapv normalize (flat %))) 9 | :else (throw (ex-info "Unsupported widget type." {:class (type widget)})))) 10 | 11 | (defn deep-merge [x y] 12 | (reduce (fn [m [k v]] 13 | (if (map? v) 14 | (update m k merge v) 15 | (assoc m k v))) 16 | x, y)) 17 | 18 | (defn merge-prop [widget [k v]] 19 | (let [f (case k 20 | :data merge 21 | :state merge 22 | :dom/props deep-merge 23 | (fn [x y] y))] 24 | (update widget k f v))) 25 | 26 | (defn inherit-prototype [widget] 27 | (reduce merge-prop (:prototype widget) (dissoc widget :prototype))) 28 | 29 | (defn assign-indexes [widget] 30 | (update widget :children #(mapv (fn [child i] 31 | (-> child 32 | (assoc :child-index i) 33 | assign-indexes)) 34 | % (range)))) 35 | 36 | (defn assign-ids [widget] 37 | (let [scope (:scope widget []) 38 | idx (:child-index widget :root) 39 | k (:key widget idx) 40 | id (:id widget [scope k]) 41 | scope (conj scope k)] 42 | (-> widget 43 | (assoc :id id) 44 | (update :children #(mapv (fn [child] 45 | (assign-ids (assoc child :scope scope))) 46 | %))))) 47 | 48 | ;;XXX Is this necessary? Desirable? Why? 49 | (defn link-children [widget] 50 | (update widget :children #(mapv :id %))) 51 | 52 | (def default-item-prototype 53 | {:template 54 | (fn [x] 55 | {:dom/tag :text ;XXX 56 | :text (pr-str (:data x))})}) 57 | 58 | (defn render-items [widget] 59 | (let [filt (:item-filter widget (constantly true))] 60 | (if-let [items (->> widget :items (filter filt) seq)] 61 | (let [proto (:item-prototype widget default-item-prototype)] 62 | (assoc widget :children (mapv #(assoc proto :item %) items))) 63 | widget))) 64 | 65 | (defn load-state [{:keys [id state] :as widget}] 66 | (update widget :state merge (state/init! id state))) 67 | 68 | (defn render-template [widget] 69 | (when-let [template (:template widget)] 70 | (merge (template widget) 71 | (select-keys widget [:child-index :key :scope])))) 72 | 73 | (defn expand [widget] 74 | (let [widget (-> widget 75 | normalize 76 | inherit-prototype 77 | render-items 78 | assign-indexes 79 | assign-ids 80 | load-state)] 81 | (if-let [rendered (render-template widget)] 82 | (merge (select-keys widget [:id :data :item :state :handler]) 83 | {:children [(expand rendered)]}) 84 | widget))) 85 | 86 | (defn expand-all [widget] 87 | (-> widget 88 | expand 89 | (update :children #(mapv expand-all %)))) 90 | 91 | ;;;TODO Implement graph representation, incremental, and life cycles. 92 | 93 | (defn add-widget [graph widget] 94 | (assoc graph (:id widget) widget)) 95 | 96 | (defn add-widget-tree [graph widget] 97 | (let [widget (expand widget) 98 | graph (reduce add-widget-tree 99 | graph 100 | (:children widget))] 101 | (add-widget graph (link-children widget)))) 102 | 103 | (def null {}) 104 | 105 | (defn widget->graph [widget] 106 | (add-widget-tree null widget)) 107 | -------------------------------------------------------------------------------- /src/proact/render/loop.cljc: -------------------------------------------------------------------------------- 1 | (ns proact.render.loop) 2 | 3 | (defonce callbacks (atom {})) 4 | 5 | (defonce timeout (atom nil)) 6 | 7 | (defn notify! [] 8 | (doseq [[_ f] @callbacks] 9 | (f)) 10 | (reset! timeout nil)) 11 | 12 | (defn trigger! [] 13 | ;;XXX requestAnimationFrame ? 14 | #?(:cljs (swap! timeout #(or % (js/setTimeout notify! 0))) 15 | :clj (assert false "Not implemented yet"))) ;XXX 16 | 17 | (defn add-listener [key f] 18 | (swap! callbacks assoc key f) 19 | (trigger!)) 20 | 21 | (defn remove-listener [key] 22 | (swap! callbacks dissoc key) 23 | nil) 24 | -------------------------------------------------------------------------------- /src/proact/render/state.cljc: -------------------------------------------------------------------------------- 1 | (ns proact.render.state 2 | (:refer-clojure :exclude [get]) 3 | (:require [proact.render.loop :as loop])) 4 | 5 | (defonce global 6 | (add-watch (atom {}) ::watch (fn [& _] (loop/trigger!)))) 7 | 8 | (defn get [id] 9 | (@global id)) 10 | 11 | (def left-merge (partial merge-with (fn [x _] x))) 12 | 13 | (defn init! [id state] 14 | ((swap! global update id left-merge state) id)) 15 | 16 | (defn put! [id state] 17 | (swap! global update id merge state) 18 | nil) 19 | 20 | (defn clear! [id] 21 | (swap! global dissoc id) 22 | nil) 23 | -------------------------------------------------------------------------------- /src/proact/util.cljc: -------------------------------------------------------------------------------- 1 | (ns proact.util) 2 | 3 | (defn flat [xs] 4 | (lazy-seq 5 | (when-first [x xs] 6 | (let [xs* (flat (next xs))] 7 | (cond 8 | (nil? x) xs* 9 | (seq? x) (concat (flat x) xs*) 10 | :else (cons x xs*)))))) 11 | -------------------------------------------------------------------------------- /src/proact/widgets/controls.cljc: -------------------------------------------------------------------------------- 1 | (ns proact.widgets.controls 2 | (:require 3 | #?(:clj [clojure.core.match :refer [match]]) 4 | #?(:cljs [cljs.core.match :refer-macros [match]]) 5 | [proact.render.state :as state])) 6 | 7 | (defn button-handler [widget e] 8 | (match [e] 9 | [[:click]] (:command widget) 10 | :else e)) 11 | 12 | (def button {:handler button-handler 13 | :command [:press]}) 14 | 15 | (defn toggle-handler [widget e] 16 | (match [e] 17 | [[:click]] (state/put! (:id widget) 18 | {:value (not (get-in widget [:state :value]))}) 19 | :else e)) 20 | 21 | (def toggle {:state {:value false} 22 | :handler toggle-handler 23 | :children []}) 24 | 25 | ;TODO expander 26 | -------------------------------------------------------------------------------- /src/proact/widgets/layout.cljc: -------------------------------------------------------------------------------- 1 | (ns proact.widgets.layout) 2 | 3 | (def flex 4 | {:dom/tag "div" 5 | :dom/props {"style" {"display" "flex"}}}) 6 | 7 | (def row (assoc-in flex [:dom/props "style" "flex-direction"] "row")) 8 | (def column (assoc-in flex [:dom/props "style" "flex-direction"] "column")) 9 | -------------------------------------------------------------------------------- /src/proact/widgets/tools.cljc: -------------------------------------------------------------------------------- 1 | (ns proact.widgets.tools 2 | (:require [proact.html :as html] ;XXX 3 | [proact.widgets.controls :as ctrl] 4 | [proact.widgets.layout :as layout])) 5 | 6 | (declare tree-view) 7 | 8 | (def entry-view 9 | {:template 10 | (fn [{[k v] :item}] 11 | {:prototype layout/row 12 | :children 13 | [(html/div {"style" {"font-weight" "bold" 14 | "padding-left" "5px" 15 | "border-left" "2px solid red"}} 16 | (assoc ctrl/toggle 17 | :template (fn [widget] 18 | (html/div {} (pr-str (:state widget))))) 19 | (pr-str k)) 20 | (assoc tree-view :item v)]})}) 21 | 22 | (def map-view 23 | {:template 24 | (fn [{:keys [item]}] 25 | {:prototype layout/column 26 | :children 27 | [{:item-prototype entry-view 28 | :items (vec item)}]})}) 29 | 30 | (def vector-view 31 | {:template 32 | (fn [{:keys [item]}] 33 | {:prototype layout/column 34 | :children 35 | [{:item-prototype entry-view 36 | :items (mapv vector (range) item)}]})}) 37 | 38 | (def scalar-view 39 | {:template 40 | (fn [{:keys [item]}] 41 | (html/div {} 42 | (let [[color text] (cond 43 | (nil? item) ["orange" "nil"] 44 | (fn? item) ["purple" "#