diff --git a/README.md b/README.md index 006ef85..228c63c 100644 --- a/README.md +++ b/README.md @@ -10,14 +10,6 @@ One day when your app will have grown and you’ll be ready to eject it, you’l You can fork this repo or clone it and have your own branch. We’ll continue to update the main branch. You’ll be able to track changes and merge them as needed. - - - - ## Quick Start ```shell @@ -139,76 +131,3 @@ $ clojure -X:build:prod uberjar :hyperfiddle/domain hello-fiddle :build/jar-name $ fly deploy --remote-only --config src/hello_fiddle/fly.toml ``` - diff --git a/deps.edn b/deps.edn index 05c3dbd..698c8d5 100644 --- a/deps.edn +++ b/deps.edn @@ -1,77 +1,58 @@ -{:paths ["src" "resources" "src-fiddles"] - :deps - {com.hyperfiddle/electric {:git/url "https://github.com/hyperfiddle/electric/" - :git/sha "0a6966891c1b009fb808add868b72c8d37268296"} - com.hyperfiddle/rcf {:git/url "https://github.com/hyperfiddle/electric" - :git/sha "7105b43231140de6f2f39ce38611c9f6c9dfc976"} - info.sunng/ring-jetty9-adapter - {:mvn/version "0.17.7" ; (Jetty 10) is NOT Java 8 compatible - :exclusions [org.slf4j/slf4j-api info.sunng/ring-jetty9-adapter-http3]} - org.clojure/clojure {:mvn/version "1.12.0-alpha4"} - org.clojure/clojurescript {:mvn/version "1.11.60"} ; technically :dev - org.clojure/tools.logging {:mvn/version "1.2.4"} - ch.qos.logback/logback-classic {:mvn/version "1.2.11"} - ring-basic-authentication/ring-basic-authentication {:mvn/version "1.1.1"} - - markdown-clj/markdown-clj {:mvn/version "1.11.4"} - nextjournal/clojure-mode {:git/url "https://github.com/nextjournal/clojure-mode" - :sha "ac038ebf6e5da09dd2b8a31609e9ff4a65e36852"}} - - :aliases +{:deps {com.hyperfiddle/electric {:git/url "https://github.com/hyperfiddle/electric/" :git/sha "0a6966891c1b009fb808add868b72c8d37268296"} + com.hyperfiddle/rcf {:git/url "https://github.com/hyperfiddle/electric" :git/sha "7105b43231140de6f2f39ce38611c9f6c9dfc976"} + org.clojure/clojure {:mvn/version "1.12.0-alpha4"} + org.clojure/clojurescript {:mvn/version "1.11.60"} ; technically :dev + org.clojure/tools.logging {:mvn/version "1.2.4"} + ch.qos.logback/logback-classic {:mvn/version "1.2.11"} + info.sunng/ring-jetty9-adapter {:mvn/version "0.17.7" ; (Jetty 10) is NOT Java 8 compatible + :exclusions [org.slf4j/slf4j-api info.sunng/ring-jetty9-adapter-http3]} + ring-basic-authentication/ring-basic-authentication {:mvn/version "1.1.1"} + } + + :paths ["src" "resources" "src-fiddles"] + + :aliases {:dev - {:extra-paths ["src-dev" "src-triage"] - :override-deps - {com.hyperfiddle/electric {:local/root "vendor/electric"} - com.hyperfiddle/rcf {:local/root "vendor/rcf"}} - :extra-deps - {binaryage/devtools {:mvn/version "1.0.6"} - io.github.clojure/tools.build - {:mvn/version "0.9.5" - :exclusions [com.google.guava/guava ; https://clojurians.slack.com/archives/C6N245JGG/p1692799642324169 - org.slf4j/slf4j-nop #_"clashes with app logger"]} - thheller/shadow-cljs {:mvn/version "2.25.2"}} - :jvm-opts - ["-XX:-OmitStackTraceInFastThrow" #_RCF - ]} + {:extra-paths ["src-dev"] + :override-deps {com.hyperfiddle/electric {:local/root "vendor/electric"} + com.hyperfiddle/rcf {:local/root "vendor/rcf"}} + :extra-deps {binaryage/devtools {:mvn/version "1.0.6"} + thheller/shadow-cljs {:mvn/version "2.25.2"} + io.github.clojure/tools.build + {:mvn/version "0.9.5" + :exclusions [com.google.guava/guava ; Guava version conflict between tools.build and clojurescript. + org.slf4j/slf4j-nop ; clashes with app logger + ]}} + :jvm-opts ["-XX:-OmitStackTraceInFastThrow" ; For RCF + ]} :build - ; use `clj -X:build build-client`, NOT -T! build/app classpath contamination cannot be prevented - ; see https://www.notion.so/hyperfiddle/logger-epic-303a8024a8fd4b09a40a67871d3161cf?pvs=4 + ;; use `clj -X:build build-client`, NOT -T! build/app classpath contamination cannot be prevented {:extra-paths ["src-build"] - :ns-default build - :extra-deps {io.github.clojure/tools.build - {:mvn/version "0.9.5" - :exclusions [com.google.guava/guava ; https://clojurians.slack.com/archives/C6N245JGG/p1692799642324169 - org.slf4j/slf4j-nop #_"clashes with app logger"]} - thheller/shadow-cljs {:mvn/version "2.25.2"}} - } + :ns-default build + :extra-deps + {thheller/shadow-cljs {:mvn/version "2.25.2"} + io.github.clojure/tools.build {:mvn/version "0.9.5" + :exclusions [com.google.guava/guava ; Guava version conflict between tools.build and clojurescript. + org.slf4j/slf4j-nop ; clashes with app logger + ]}}} :prod {:extra-paths ["src-prod"]} - ;;;;;;;;;;;;;;;;;;;; - ;; Fiddle Aliases ;; - ;;;;;;;;;;;;;;;;;;;; + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + ;; Fiddle Aliases ;; + ;; Add your custom extra deps and configurations below ;; + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; :hello-fiddle {} :electric-tutorial {:extra-deps - {datascript/datascript {:mvn/version "1.5.2"} - reagent/reagent {:mvn/version "1.1.1"}}} - - :datomic-browser - {:extra-deps - {com.datomic/peer {:mvn/version "1.0.7075"}}} - - :dustingetz - {:extra-deps - {com.datomic/peer {:mvn/version "1.0.7075"} - com.hyperfiddle/hfql {:local/root "vendor/hfql"}}} + {datascript/datascript {:mvn/version "1.5.2"} + reagent/reagent {:mvn/version "1.1.1"} + markdown-clj/markdown-clj {:mvn/version "1.11.4"} + nextjournal/clojure-mode {:git/url "https://github.com/nextjournal/clojure-mode" :git/sha "ac038ebf6e5da09dd2b8a31609e9ff4a65e36852"}}} - :hfql-demo - {:extra-paths ["src-triage"] - :extra-deps - {com.datomic/peer {:mvn/version "1.0.7075"} - com.hyperfiddle/hfql {:local/root "vendor/hfql"}}}}} + }} diff --git a/resources/public/hyperfiddle/hfql-tree-grid.css b/resources/public/hyperfiddle/hfql-tree-grid.css deleted file mode 120000 index 1efa756..0000000 --- a/resources/public/hyperfiddle/hfql-tree-grid.css +++ /dev/null @@ -1 +0,0 @@ -../../../vendor/hfql/resources/public/hyperfiddle/hfql/hfql-tree-grid.css \ No newline at end of file diff --git a/resources/public/hyperfiddle/hyperfiddle-electric-ui-copy.css b/resources/public/hyperfiddle/hyperfiddle-electric-ui-copy.css deleted file mode 100644 index be622ff..0000000 --- a/resources/public/hyperfiddle/hyperfiddle-electric-ui-copy.css +++ /dev/null @@ -1,51 +0,0 @@ -.hyperfiddle button[aria-disabled=true], input[type="button"][aria-disabled="true"]{ - color: GrayText; -} - -.hyperfiddle button[aria-busy=true] -, input[type=checkbox][aria-busy=true] -{ - cursor:wait; - position: relative; -} - -/* Button spinner */ -.hyperfiddle button[aria-busy=true]::before -, .hyperfiddle input[type=checkbox][aria-busy=true]::after -, .hyperfiddle .input-load-mask[aria-busy=true]::after -{ - content:""; - position:absolute; - z-index: 1; - width: 0.9em; - height: 0.9em; - margin: auto; - top:0; - bottom:0; - left:0; - right:0; - animation: hyperfiddle-spinner-spin 1s linear infinite; - border-width: 2px; - border-style: solid; - border-left-color: transparent; - border-radius: 50%; -} - -.hyperfiddle .input-load-mask[aria-busy=true] { - position: relative; -} -.hyperfiddle .input-load-mask[aria-busy=true]::after{ - left: auto; - right: 1rem; -} - -/* Button spinner color */ -.hyperfiddle button[aria-busy=true]::before{ - border-color: initial; - border-left-color: transparent; -} - -@keyframes hyperfiddle-spinner-spin{ - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } -} diff --git a/resources/public/hyperfiddle/hyperfiddle-forms-copy.css b/resources/public/hyperfiddle/hyperfiddle-forms-copy.css deleted file mode 100644 index 84abd07..0000000 --- a/resources/public/hyperfiddle/hyperfiddle-forms-copy.css +++ /dev/null @@ -1,171 +0,0 @@ -.hyperfiddle input:not([type="radio"]):not([type="checkbox"]), select { - box-sizing: border-box; - /* width:100%; */ - border-width: 1px; - /*border-left-width: 0.2rem;*/ - border-style:solid; - border-radius: 2px; - /* padding: 0.5rem 0.5rem; */ - width: 100%; -} - -.hyperfiddle input[type="checkbox"]{ - margin:0; - width: 1rem; - height: 1rem; -} - -.hyperfiddle table { - table-layout: fixed; - border-collapse: collapse; -} - - -.hyperfiddle .hyperfiddle-select -, .hyperfiddle .hyperfiddle-typeahead -, .hyperfiddle .hyperfiddle-tag-picker -{ - /* background-color: red; */ - position: relative; -} - -.hyperfiddle .hyperfiddle-select input, -.hyperfiddle .hyperfiddle-typeahead input -{ - border: none; - border-style: none !important; -} - -.hyperfiddle .hyperfiddle-select > ul -, .hyperfiddle .hyperfiddle-typeahead > ul -, .hyperfiddle .hyperfiddle-tag-picker-input-container > ul -{ - margin: 0; - padding: 0.25rem 0; - position:absolute; - width: 100%; - z-index: 2; - list-style-type: none; - background-color: white; - box-shadow: 0 0.5rem 1rem gray; - - display: grid; - grid-gap: var(--hf-grid-gap); -} - -.hyperfiddle .hyperfiddle-select > ul > li -, .hyperfiddle .hyperfiddle-typeahead > ul > li -, .hyperfiddle .hyperfiddle-tag-picker-input-container > ul > li -{ - height: var(--hf-grid-row-height); - padding: 0 0.5rem; - -} - -.hyperfiddle .hyperfiddle-select > ul > li:hover -, .hyperfiddle .hyperfiddle-typeahead > ul > li:hover -, .hyperfiddle .hyperfiddle-tag-picker-input-container > ul > li:hover -{ - cursor: pointer; -} - -.hyperfiddle .hyperfiddle-tag-picker -, .hyperfiddle .hyperfiddle-tag-picker-input-container -, .hyperfiddle .hyperfiddle-tag-picker-items -, .hyperfiddle .hyperfiddle-tag-picker-items > li -{ - display: inline-block; -} - -.hyperfiddle .hyperfiddle-tag-picker-items -{ - list-style-type: none; -} - -.hyperfiddle .hyperfiddle-tag-picker{ - display: inline-flex; - flex-direction: row; - flex-wrap: wrap; - gap: 0.25rem 0.5rem; - align-items:center; - padding: 0.25rem; - background: white; -} - -.hyperfiddle .hyperfiddle-tag-picker:focus-within{ - box-shadow: inset 0 2px 4px 0 rgb(0 0 0 / 0.05); -} - -.hyperfiddle .hyperfiddle-tag-picker-items{ - display:contents; -} - -.hyperfiddle .hyperfiddle-tag-picker-items > li -{ - border: 1px solid; - box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); - white-space: nowrap; -} - -.hyperfiddle .hyperfiddle-tag-picker-input-container{ - position: relative; - height: 100%; - max-height: 100%; - padding:0; - -} - -.hyperfiddle .hyperfiddle-tag-picker-input-container > input{ - height: 100%; - border-width:0!important; /* TODO drop the !important once we have a clean CSS system (tailwind) */ - outline: none; -} -.hyperfiddle .hyperfiddle-tag-picker-input-container > ul{ - position: absolute; - width: max-content; - margin-top: 0.25rem; - border-radius: 0 0 0.25rem 0.25rem; - box-shadow: none; - box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); -} - -.hyperfiddle .hyperfiddle-tag-picker-items > li { - border-radius: 0.25rem; - padding: 0 0 0 0.5rem; -} - -.hyperfiddle .hyperfiddle-tag-picker-items > li > span { - margin: 0.25rem; - color: rgb(55 65 81); - cursor:pointer; -} - -.hyperfiddle .hyperfiddle-selected -{ - background-color: orange; -} - -.hyperfiddle .hyperfiddle-modal-backdrop -{ - display: block; - /* background-color: pink; */ - /* opacity: 0.1; */ - position: fixed; - top:0; - bottom:0; - left:0; - right:0; - z-index:1; -} - -.hyperfiddle table td{ - vertical-align: top; - padding: 0.3rem; - border-width: 1px; - border-style: solid; -} - -.hyperfiddle table thead td:empty{ - visibility:hidden; - border: none; -} diff --git a/resources/public/hyperfiddle/hyperfiddle-popover-copy.css b/resources/public/hyperfiddle/hyperfiddle-popover-copy.css deleted file mode 100644 index be1ce25..0000000 --- a/resources/public/hyperfiddle/hyperfiddle-popover-copy.css +++ /dev/null @@ -1,20 +0,0 @@ -.hyperfiddle.popover-wrapper{ - display: inline-block; - position: relative; -} - -.hyperfiddle.popover-body{ - position: absolute; - z-index: 2; - width: max-content; - - border: 1px pink solid; - padding: 0.5rem; - background-color: rgb(248 250 252); - box-shadow: 0 0 1rem lightgray; -} - -.hyperfiddle.popover-body:focus-within{ - /* NOTE popover is itself focusable so a click brings it to the top */ - z-index:3; -} diff --git a/resources/public/index.css b/resources/public/index.css index 773a441..ccca2d7 100644 --- a/resources/public/index.css +++ b/resources/public/index.css @@ -1,12 +1,5 @@ /* TODO migrate to Tailwind */ -/* Dustin: Ring intermittently failing to follow symlinks, so I copied the -contents. Note the hfql-tree-grid.css symlink is working. */ -@import url('hyperfiddle/hyperfiddle-forms-copy.css'); -@import url('hyperfiddle/hyperfiddle-electric-ui-copy.css'); -@import url('hyperfiddle/hyperfiddle-popover-copy.css'); -@import url('hyperfiddle/hfql-tree-grid.css'); - @import url('user/examples.css'); @import url('user/tutorial.css'); @import url('user/github-markdown.css'); diff --git a/resources/public/todo-list.css b/resources/public/todo-list.css deleted file mode 100644 index ac1764f..0000000 --- a/resources/public/todo-list.css +++ /dev/null @@ -1,58 +0,0 @@ -@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@100;200;300;400;500;600;700&display=swap'); - -body{ - font-family: 'IBM Plex Sans', sans-serif; - font-weight: 400; -} - -input[type="text"]{ - padding: 0.5rem; - margin-bottom: 1rem; - min-width: 30rem; - border-radius: 0.25rem; - border: 1px gray solid; -} - -input[type="checkbox"]{ - width: 1rem; - height: 1rem; -} - -input[type="checkbox"]:checked + label{ - text-decoration: line-through; - color: gray; -} - -label{ - padding-left: 0.5rem; -} - -label, input[type="checkbox"]{ - cursor:pointer; - transition: 0.4s ease all; -} - -label::first-letter{ - text-transform: capitalize; -} - -.todo-list{ - display:grid; - grid-template-rows: auto fit-content auto; - max-width: 30rem; -} - -.todo-items{ - display:grid; - grid-template-columns: auto 1fr; - grid-gap: 0.5rem 0; - padding: 0 0.25rem; -} - -.counter{ - justify-self: center; -} - -.count{ - font-size: 1.25rem; -} \ No newline at end of file diff --git a/resources/public/user/demo_stage_ui4.css b/resources/public/user/demo_stage_ui4.css deleted file mode 100644 index c212de9..0000000 --- a/resources/public/user/demo_stage_ui4.css +++ /dev/null @@ -1,5 +0,0 @@ -.wip-demo-stage-ui4-staged { - display: block; - width: 100%; - height: 10em; -} \ No newline at end of file diff --git a/src-fiddles/datomic_browser/datomic_browser.cljc b/src-fiddles/datomic_browser/datomic_browser.cljc deleted file mode 100644 index 2c5a889..0000000 --- a/src-fiddles/datomic_browser/datomic_browser.cljc +++ /dev/null @@ -1,212 +0,0 @@ -(ns datomic-browser.datomic-browser - (:require clojure.edn - contrib.ednish - [contrib.str :refer [any-matches?]] - [contrib.data :refer [unqualify treelister]] - #?(:clj [contrib.datomic-contrib :as dx]) - [contrib.datomic-m #?(:clj :as :cljs :as-alias) d] - [contrib.gridsheet :as gridsheet :refer [Explorer]] - [datomic-browser.domain :as D :refer [db conn schema]] - #?(:clj datomic.api) - [hyperfiddle.electric :as e] - [hyperfiddle.electric-dom2 :as dom] - [hyperfiddle.history :as history] - [missionary.core :as m])) - -(comment (ns-unmap *ns* 'model)) - -(e/defn RecentTx [] - (e/client (dom/h1 (dom/text "Recent Txs"))) - (e/server - (Explorer. - (treelister (new (->> (d/datoms> db {:index :aevt, :components [:db/txInstant]}) - (m/reductions conj ()) - (m/relieve {}))) - (fn [_]) any-matches?) - {::gridsheet/page-size 30 - ::gridsheet/row-height 24 - ::gridsheet/columns [:db/id :db/txInstant] - ::gridsheet/grid-template-columns "10em auto" - ::gridsheet/Format - (e/fn [[e _ v tx op :as record] a] - (case a - :db/id (e/client (history/link [::tx tx] (dom/text tx))) - :db/txInstant (e/client (dom/text (pr-str v))) #_(e/client (.toLocaleDateString v))))}))) - -(e/defn Attributes [] - (e/client (dom/h1 (dom/text "Attributes"))) - (e/server - (let [cols [:db/ident :db/valueType :db/cardinality :db/unique :db/isComponent - #_#_#_#_:db/fulltext :db/tupleType :db/tupleTypes :db/tupleAttrs]] - (Explorer. - (treelister (->> (dx/attributes> db cols) - (m/reductions conj []) - (m/relieve {}) - new - (sort-by :db/ident)) ; sort by db/ident which isn't available - (fn [_]) any-matches?) - {::gridsheet/page-size 15 - ::gridsheet/row-height 24 - ::gridsheet/columns cols - ::gridsheet/grid-template-columns "auto 6em 4em 4em 4em" - ::gridsheet/Format - (e/fn [row col] - (e/client - (let [v (col row)] - (case col - :db/ident (history/link [::attribute v] (dom/text v)) - :db/valueType (some-> v :db/ident name dom/text) - :db/cardinality (some-> v :db/ident name dom/text) - :db/unique (some-> v :db/ident name dom/text) - (dom/text (str v))))))})))) - -(e/defn Format-entity [[k v :as row] col] - (e/server - (assert (some? schema)) - (case col - ::k (cond - (= :db/id k) (e/client (dom/text k)) ; :db/id is our schema extension, can't nav to it - (contains? schema k) (e/client (history/link [::attribute k] (dom/text k))) - () (e/client (dom/text (str k)))) ; str is needed for Long db/id, why? - ::v (if-not (coll? v) ; don't render card :many intermediate row - (let [[valueType cardinality] - ((juxt (comp unqualify dx/identify :db/valueType) - (comp unqualify dx/identify :db/cardinality)) (k schema))] - (cond - (= :db/id k) (e/client (history/link [::entity v] (dom/text v))) - (= :ref valueType) (e/client (history/link [::entity v] (dom/text v))) - () (e/client (dom/text (pr-str v))))))))) - -(e/defn EntityDetail [e] - (assert e) - (e/client (dom/h1 (dom/text "Entity detail: " e))) ; treeview on the entity - (e/server - (Explorer. - ;; TODO inject sort - (treelister (new (e/task->cp (d/pull db {:eid e :selector ['*] :compare compare}))) - (partial dx/entity-tree-entry-children schema) - any-matches?) - {::gridsheet/page-size 15 - ::gridsheet/row-height 24 - ::gridsheet/columns [::k ::v] - ::gridsheet/grid-template-columns "15em auto" - ::gridsheet/Format Format-entity}))) - -(e/defn EntityHistory [e] - (assert e) - (e/client (dom/h1 (dom/text "Entity history: " (pr-str e)))) - (e/server - (Explorer. - ; accumulate what we've seen so far, for pagination. Gets a running count. Bad? - (treelister (new (->> (dx/entity-history-datoms> db e) - (m/reductions conj []) ; track a running count as well? - (m/relieve {}))) - (fn [_]) any-matches?) - {::gridsheet/page-size 20 - ::gridsheet/row-height 24 - ::gridsheet/columns [::e ::a ::op ::v ::tx-instant ::tx] - ::gridsheet/grid-template-columns "10em 10em 3em auto auto 9em" - ::gridsheet/Format - (e/fn [[e aa v tx op :as row] a] - (when row ; when this view unmounts, somehow this fires as nil - (case a - ::op (e/client (dom/text (name (case op true :db/add false :db/retract)))) - ::e (e/client (history/link [::entity e] (dom/text e))) - ::a (if (some? aa) - (let [ident (:db/ident (new (e/task->cp (d/pull db {:eid aa :selector [:db/ident]}))))] - (e/client (dom/text (pr-str ident))))) - ::v (e/client (some-> v pr-str dom/text)) - ::tx (e/client (history/link [::tx tx] (dom/text tx))) - ::tx-instant (let [x (:db/txInstant (new (e/task->cp (d/pull db {:eid tx :selector [:db/txInstant]}))))] - (e/client (pr-str (dom/text x)))) - (str v))))}))) - -(e/defn AttributeDetail [a] - (e/client (dom/h1 (dom/text "Attribute detail: " a))) - (e/server - (Explorer. - (treelister (new (->> (d/datoms> db {:index :aevt, :components [a]}) - (m/reductions conj []) - (m/relieve {}))) - (fn [_]) any-matches?) - {::gridsheet/page-size 20 - ::gridsheet/row-height 24 - ::gridsheet/columns [:e :a :v :tx] - ::gridsheet/grid-template-columns "15em 15em calc(100% - 15em - 15em - 9em) 9em" - ::gridsheet/Format - (e/fn [[e _ v tx op :as x] k] - (e/client - (case k - :e (history/link [::entity e] (dom/text e)) - :a (dom/text (pr-str a)) #_(let [aa (new (e/task->cp (dx/ident! db aa)))] aa) - :v (some-> v str dom/text) ; todo when a is ref, render link - :tx (history/link [::tx tx] (dom/text tx)))))}))) - -(e/defn TxDetail [e] - (e/client (dom/h1 (dom/text "Tx detail: " e))) - (e/server - (Explorer. - (treelister (new (->> (d/tx-range> conn {:start e, :end (inc e)}) ; global - (m/eduction (map :data) cat) - (m/reductions conj []) - (m/relieve {}))) - (fn [_]) any-matches?) - {::gridsheet/page-size 20 - ::gridsheet/row-height 24 - ::gridsheet/columns [:e :a :v :tx] - ::gridsheet/grid-template-columns "15em 15em calc(100% - 15em - 15em - 9em) 9em" - ::gridsheet/Format - (e/fn [[e aa v tx op :as x] a] - (case a - :e (let [e (new (e/task->cp (dx/ident! db e)))] (e/client (history/link [::entity e] (dom/text e)))) - :a (let [aa (new (e/task->cp (dx/ident! db aa)))] (e/client (history/link [::attribute aa] (dom/text aa)))) - :v (pr-str v) ; when a is ref, render link - (str tx)))}))) - -(e/defn DbStats [] - (e/client (dom/h1 (dom/text "Db stats"))) - (e/server - (Explorer. - (treelister - (new (e/task->cp (d/db-stats db))) - (fn [[k v]] (condp = k :attrs (into (sorted-map) v) nil)) - any-matches?) - {::gridsheet/page-size 20 - ::gridsheet/row-height 24 - ::gridsheet/columns [::k ::v] - ::gridsheet/grid-template-columns "20em auto" - ::gridsheet/Format - (e/fn [[k v :as row] col] - (e/client - (case col - ::k (dom/text (pr-str k)) - ::v (cond - (= k :attrs) nil ; print children instead - () (dom/text (pr-str v))))))}))) ; {:count 123} - -(comment - {:datoms 800958, - :attrs - {:release/script {:count 11435}, - :label/type {:count 870} - ... ...}}) - -(e/defn DatomicBrowser [& [page x]] - (e/client - (dom/h1 (dom/text "Datomic browser")) - (dom/link (dom/props {:rel :stylesheet, :href "gridsheet-optional.css"})) - (dom/div (dom/props {:class "user-gridsheet-demo"}) - (dom/div (dom/text "Nav: ") - (history/link [::summary] (dom/text "home")) (dom/text " ") - (history/link [::db-stats] (dom/text "db-stats")) (dom/text " ") - (history/link [::recent-tx] (dom/text "recent-tx"))) - (case (or page ::summary) - ::summary (history/router 1 (e/server (Attributes.))) - ::attribute (history/router 2 (e/server (AttributeDetail. x))) - ::tx (history/router 2 (e/server (TxDetail. x))) - ::entity (do (history/router 2 - (history/router ::entity-detail (e/server (EntityDetail. x))) - (history/router ::entity-history (e/server (EntityHistory. x))))) - ::db-stats (history/router 1 (e/server (DbStats.))) - ::recent-tx (history/router 1 (e/server (RecentTx.))) - (e/client (dom/text "no matching route: " (pr-str page))))))) diff --git a/src-fiddles/datomic_browser/domain.cljc b/src-fiddles/datomic_browser/domain.cljc deleted file mode 100644 index 076b4fd..0000000 --- a/src-fiddles/datomic_browser/domain.cljc +++ /dev/null @@ -1,6 +0,0 @@ -(ns datomic-browser.domain - (:require [hyperfiddle.electric :as e])) - -(e/def conn) -(e/def db) -(e/def schema) diff --git a/src-fiddles/datomic_browser/fiddles.cljc b/src-fiddles/datomic_browser/fiddles.cljc deleted file mode 100644 index 9a7484b..0000000 --- a/src-fiddles/datomic_browser/fiddles.cljc +++ /dev/null @@ -1,23 +0,0 @@ -(ns datomic-browser.fiddles - (:require [contrib.assert :refer [check]] - [contrib.clojurex :refer [bindx]] - #?(:clj [contrib.datomic-contrib :as dx]) - electric-fiddle.main - [hyperfiddle :as hf] - [hyperfiddle.electric :as e] - [datomic-browser.domain :refer [conn db schema]] ; :( - [datomic-browser.datomic-browser :refer [DatomicBrowser]] - #_models.mbrainz - #?(:clj [models.teeshirt-orders-datomic :as model]))) - -(e/defn FiddleMain [& [page x]] - (e/server - (bindx [conn (check (model/init-datomic)) - db (check model/*$*) - schema (check (new (dx/schema> db)))] - ; index-route (or (seq args) [::summary]) - (e/client - (DatomicBrowser. page x))))) - -(e/def fiddles - {`DatomicBrowser FiddleMain}) diff --git a/src-fiddles/dustingetz/demo_explorer_hfql.cljc b/src-fiddles/dustingetz/demo_explorer_hfql.cljc deleted file mode 100644 index 50fb592..0000000 --- a/src-fiddles/dustingetz/demo_explorer_hfql.cljc +++ /dev/null @@ -1,50 +0,0 @@ -(ns dustingetz.demo-explorer-hfql - (:require [clojure.datafy :refer [datafy]] - [clojure.core.protocols :refer [nav]] - #?(:clj clojure.java.io) - [clojure.spec.alpha :as s] - [contrib.datafy-fs #?(:clj :as :cljs :as-alias) fs] - #?(:clj datomic.api) - [hyperfiddle.api :as hf] - [hyperfiddle.hfql :refer [hfql]] - [hyperfiddle.electric :as e] - [hyperfiddle.electric-dom2 :as dom] - [hyperfiddle.history :as history] - [hyperfiddle.hfql-tree-grid :refer [with-gridsheet-renderer]])) - -(def unicode-folder "\uD83D\uDCC2") ; 📂 - -(e/defn DirectoryExplorer-HFQL [] - (with-gridsheet-renderer - (binding [hf/db-name "$"] - (dom/style {:grid-template-columns "repeat(5, 1fr)"}) - (e/server - (binding [hf/*nav!* (fn [db e a] (a (datafy e))) ;; FIXME db is specific, hfql should be general - hf/*schema* (constantly nil)] ;; FIXME this is datomic specific, hfql should be general - (let [path (fs/absolute-path "node_modules")] - (hfql {(props (fs/list-files (props path {::dom/disabled true})) ;; FIXME forward props - {::hf/height 30}) - [(props ::fs/name #_{::hf/render (e/fn [{::hf/keys [Value]}] - (let [v (Value.)] - (case (::fs/kind m) - ::fs/dir (let [absolute-path (::fs/absolute-path m)] - (e/client (history/Link. [::fs/dir absolute-path] v))) - (::fs/other ::fs/symlink ::fs/unknown-kind) v - v #_(e/client (router/Link. [::fs/file x] v)))))}) - - ;; TODO add links and indentation - - (props ::fs/modified {::hf/render (e/fn [{::hf/keys [Value]}] - (e/client - (dom/text - (-> (e/server (Value.)) - .toISOString - (.substring 0 10)))))}) - ::fs/size - (props ::fs/kind {::hf/render (e/fn [{::hf/keys [Value]}] - (let [v (Value.)] - (e/client - (case v - ::fs/dir (dom/text unicode-folder) - (dom/text (some-> v name))))))}) - ]}))))))) diff --git a/src-fiddles/dustingetz/electric_y_combinator.md b/src-fiddles/dustingetz/electric_y_combinator.md deleted file mode 100644 index 303515f..0000000 --- a/src-fiddles/dustingetz/electric_y_combinator.md +++ /dev/null @@ -1,59 +0,0 @@ -# Electric Y Combinator — Electric Clojure - -by Dustin Getz, 2023 July 24 - -[Electric Clojure](https://github.com/hyperfiddle/electric/) is a new way to write reactive web apps in Clojure/Script which advertises **strong composition across the frontend/backend boundary**, i.e. network transparent composition (see [wikipedia: Network transparency](https://en.wikipedia.org/wiki/Network_transparency)). - -As a proof of strong composition, and a demonstration of how this is different from weaker forms of composition like React Server Components, we offer **distributed Y-combinator** and a demo of using it to recursively walk a server filesystem hierarchy and render a browser HTML frontend, in one pass. - -If this is your first time seeing Electric, start with the [live tutorial](https://electric.hyperfiddle.net/). - -### Figure 1: Fibonacci with Electric Y Combinator - -!fiddle-ns[](dustingetz.y-fib/Y-fib) - -What's happening -- Recursive fibonacci -- The recursion is traced to the dom by side effect -- There is **no self-recursion**: we use `Y` to inject recursion into `Fib` via higher order fn `Recur` rather than `Fib` calling itself directly by its name. -- As a reminder, Electric functions are called with `new` -- there is lexical closure, e.g. `Gen` and `F` inside `Y` definition - -Key ideas -- The **Y Combinator** (wikipedia: [Fixed-point combinator](https://en.wikipedia.org/wiki/Fixed-point_combinator)) captures the essence of recursive iteration in a simple lambda expression. ChatGPT can tell you more about this. -- Y works in Electric Clojure, demonstrating that Electric lambda *is* lambda. -- Note: you don't actually need `Y` for recursion, this is just a demo. - -Ok, lets try something harder: - -### Figure 2: Recursive walk over server file system, streamed to DOM - -!fiddle-ns[](dustingetz.y-dir/Y-dir) - -What's happening -- Recursive file system traversal over "./src" directory on the server -- there's a DOM text input that filters the tree, try typing "electric", both client and server refresh live -- **direct frontend/backend composition**: the tree walker, `Dir-tree`, interweaves `e/client` and `e/server` forms arbitrarily. e.g. L26 composes `dom/li` with `file-get-name`, the filename streams across the boundary in mid-flight. The network topology is complex, and irrelevant – Electric takes care of it. -- **automatic incremental/streaming network** (i.e. **streaming lexical scope**), not request/response. No dataloaders. -- **reactive recursion**: the DOM resources are reused across reaction frames, they are not recreated unnecessarily. - - I.e., when we type into the input, the DOM change is minimized - - this minimization is not implemented in electric-dom but rather emergent from the *evaluation model of the language* - - the actual "reactive stack frames" are reused - - the DOM resource lifecycle (mount/unmount) is simply bound to the lifecycle of the reactive frame they exist in! - -Key ideas -- **network-transparent composition**: the Electric functions transmit data over the network (as implied by the AST) in a way which is invisible to the application programmer -- **strong composition**: it's actual lambda, which means it scales with complexity. Higher order functions, closures, recursion, are the exact primitive you need to build rigorous abstractions that don't leak. - - -# Conclusion - -Electric Clojure's goal is to raise the abstraction ceiling in web development, by making it so that your application logic can be expressed out of nothing but lambda. And we really mean it. Remember "Functional Core Imperative Shell"? Where is the imperative shell here? - -Lambda is the difference between abstraction and boilerplate. I challenge you to take a hard look at your current web project, stare at whatever convoluted machine you have to deal with this week, and ask yourself: Why does this exist? What artificial problem is preventing this from being a 20 line expression? - -P.S. You'll notice in the URL that we've essentially URL-encoded an S-expression and routed to it. Why do you suppose we do that? ... *This essay is a function.* - ---- - -To learn more about Electric, check out the [live tutorial](https://electric.hyperfiddle.net/), the [github repo](https://github.com/hyperfiddle/electric/) or [clone the starter app](https://github.com/hyperfiddle/electric-starter-app) and start playing. \ No newline at end of file diff --git a/src-fiddles/dustingetz/essay.cljc b/src-fiddles/dustingetz/essay.cljc deleted file mode 100644 index 7393e12..0000000 --- a/src-fiddles/dustingetz/essay.cljc +++ /dev/null @@ -1,27 +0,0 @@ -(ns dustingetz.essay - (:require clojure.string - [electric-fiddle.fiddle :refer [Fiddle-fn Fiddle-ns]] - [electric-fiddle.fiddle-markdown :refer [Custom-markdown]] - [electric-fiddle.index :refer [Index]] - [hyperfiddle :as hf] - [hyperfiddle.electric :as e] - [hyperfiddle.electric-dom2 :as dom] - [hyperfiddle.history :as history])) - -(def essays - {'electric-y-combinator "src-fiddles/dustingetz/electric_y_combinator.md" - 'hfql-intro "src-fiddles/dustingetz/hfql_intro.md" - 'hfql-teeshirt-orders "src-fiddles/dustingetz/hfql_teeshirt_orders.md"}) - -(e/def extensions - {'fiddle Fiddle-fn - 'fiddle-ns Fiddle-ns}) - -(e/defn Essay [& [?essay]] - #_(e/client (dom/div #_(dom/props {:class ""}))) ; fix css grid next - (e/client - (let [essay-filename (get essays ?essay)] - (cond - (nil? ?essay) (binding [hf/pages essays] (Index.)) - (nil? essay-filename) (dom/h1 (dom/text "Essay not found: " history/route)) - () (Custom-markdown. extensions essay-filename))))) diff --git a/src-fiddles/dustingetz/fiddles.cljc b/src-fiddles/dustingetz/fiddles.cljc deleted file mode 100644 index eabec8f..0000000 --- a/src-fiddles/dustingetz/fiddles.cljc +++ /dev/null @@ -1,39 +0,0 @@ -(ns dustingetz.fiddles - (:require [hyperfiddle.electric :as e] - [hyperfiddle :as hf] - [dustingetz.demo-explorer-hfql :refer [DirectoryExplorer-HFQL]] - [dustingetz.hfql-intro :refer [With-HFQL-Bindings - Teeshirt-orders-1 - Teeshirt-orders-2 - Teeshirt-orders-3 - Teeshirt-orders-4 - Teeshirt-orders-5]] - dustingetz.scratch - [dustingetz.y-fib :refer [Y-fib]] - [dustingetz.y-dir :refer [Y-dir]] - [dustingetz.essay :refer [Essay]] - [electric-fiddle.main] - #?(:clj models.teeshirt-orders-datomic) - )) - -(e/def fiddles - {`Y-fib Y-fib - `Y-dir Y-dir - `Essay (With-HFQL-Bindings. Essay) - `Teeshirt-orders-1 (With-HFQL-Bindings. Teeshirt-orders-1) - `Teeshirt-orders-2 (With-HFQL-Bindings. Teeshirt-orders-2) - `Teeshirt-orders-3 (With-HFQL-Bindings. Teeshirt-orders-3) - `Teeshirt-orders-4 (With-HFQL-Bindings. Teeshirt-orders-4) - `Teeshirt-orders-5 (With-HFQL-Bindings. Teeshirt-orders-5) - `DirectoryExplorer-HFQL (With-HFQL-Bindings. DirectoryExplorer-HFQL) - `dustingetz.scratch/Scratch dustingetz.scratch/Scratch}) - -#?(:clj - (models.teeshirt-orders-datomic/init-datomic)) - -(e/defn FiddleMain [ring-req] - (e/client - (binding [hf/pages fiddles] - (e/server - (electric-fiddle.main/Main. ring-req))))) - diff --git a/src-fiddles/dustingetz/fly.toml b/src-fiddles/dustingetz/fly.toml deleted file mode 100644 index 8c07a82..0000000 --- a/src-fiddles/dustingetz/fly.toml +++ /dev/null @@ -1,31 +0,0 @@ -app = "electric-fiddle" -primary_region = "ewr" -kill_signal = "SIGINT" -kill_timeout = "5s" - -[experimental] - auto_rollback = true - -[[services]] - protocol = "tcp" - internal_port = 8080 - processes = ["app"] - - [[services.ports]] - port = 80 - handlers = ["http"] - force_https = true - - [[services.ports]] - port = 443 - handlers = ["tls", "http"] - [services.concurrency] - type = "connections" - hard_limit = 200 - soft_limit = 150 - - [[services.tcp_checks]] - interval = "15s" - timeout = "2s" - grace_period = "1s" - restart_limit = 0 diff --git a/src-fiddles/dustingetz/gender_shirtsize.cljc b/src-fiddles/dustingetz/gender_shirtsize.cljc deleted file mode 100644 index 26509fb..0000000 --- a/src-fiddles/dustingetz/gender_shirtsize.cljc +++ /dev/null @@ -1,47 +0,0 @@ -(ns dustingetz.gender-shirtsize - (:require [hyperfiddle.electric :as e] - [hyperfiddle.electric-dom2 :as dom])) - -(declare orders genders shirt-sizes) - -(e/defn Teeshirt-orders-1 [_] - (e/client - (dom/table - (e/server - (e/for-by :db/id [record (orders "")] - (let [{:keys [db/id - order/email - order/gender - order/shirt-size]} record] - (e/client - (dom/tr - (dom/td (dom/input id)) - (dom/td (dom/input email)) - (dom/td (dom/select - :value gender - :options (e/fn [filter] - (e/server (genders filter))))) - (dom/td (dom/select - :value shirt-size - :options (e/fn [filter] - (e/server (shirt-sizes gender filter))))))))))))) - - -(defn Teeshirt-orders-1 [_] - (let [filter (dom/input)] - (dom/table - (for [{:keys [db/id - order/email - order/gender - order/shirt-size]} (orders filter)] - (dom/tr - (dom/td (dom/input id)) - (dom/td (dom/input email)) - (dom/td (dom/select - :value gender - :options (fn [filter] - (genders filter)))) - (dom/td (dom/select - :value shirt-size - :options (fn [filter] - (shirt-sizes gender filter))))))))) \ No newline at end of file diff --git a/src-fiddles/dustingetz/hfql_intro.cljc b/src-fiddles/dustingetz/hfql_intro.cljc deleted file mode 100644 index 3acded8..0000000 --- a/src-fiddles/dustingetz/hfql_intro.cljc +++ /dev/null @@ -1,121 +0,0 @@ -(ns dustingetz.hfql-intro - (:require [clojure.spec.alpha :as s] - [contrib.electric-codemirror :refer [CodeMirror]] - contrib.str - #?(:clj [datomic.api :as d]) - [electric-fiddle.index :refer [Index]] - [hyperfiddle.api :as hf] - [hyperfiddle.electric :as e] - [hyperfiddle.electric-dom2 :as dom] - [hyperfiddle.hfql :refer [hfql]] - [hyperfiddle.hfql-tree-grid :refer [with-gridsheet-renderer]] - [hyperfiddle.rcf :as rcf :refer [tests tap % with]] - #?(:clj [models.teeshirt-orders-datomic :as model - :refer [orders genders shirt-sizes]]))) - -(e/defn Codemirror-edn [x] - (CodeMirror. {:parent dom/node :readonly true} identity contrib.str/pprint-str - x)) - -(e/defn Teeshirt-orders-1 [] - (Codemirror-edn. - (e/server - (hfql [hf/*$* hf/db] - {(orders "") - [:db/id - :order/email]})))) - -(e/defn Teeshirt-orders-2 [] - (with-gridsheet-renderer - (e/server - (hfql [hf/*$* hf/db] - {(orders .) - [:db/id - :order/email]})))) - -(e/defn Teeshirt-orders-3 [] - (with-gridsheet-renderer - (e/server - (hfql [hf/*$* hf/db] - {(orders .) - [:db/id - :order/email - :order/gender - :order/shirt-size]})))) - -(e/defn Teeshirt-orders-4 [] - #_ - (e/client - (dom/table - (e/server - (e/for-by :db/id [record (orders "")] - (let [{:keys [db/id - order/email - order/gender - order/shirt-size]} record] - (e/client - (dom/tr - (dom/td (dom/input id)) - (dom/td (dom/input email)) - (dom/td (dom/select - :value gender - :options (e/fn [filter] - (e/server (genders filter))))) - (dom/td (dom/select - :value shirt-size - :options (e/fn [filter] - (e/server (shirt-sizes gender filter))))))))))))) - -(e/defn Teeshirt-orders-5 [] - (with-gridsheet-renderer - (e/server - (hfql [hf/*$* hf/db] - {(orders .) - [:db/id - :order/email - :order/gender - :order/shirt-size]})))) - -(e/defn With-HFQL-Bindings [F & args] - (e/client - (e/fn [& args'] - (e/server - (binding [hf/db model/*$* ; hfql compiler - hf/*nav!* model/nav! ; hfql compiler - hf/*schema* model/-schema ; hfql gridsheet renderer - ] - (e/client - (e/apply F (concat args args')))))))) - -(e/defn Scratch [_] - (e/client - (dom/h1 (dom/text "hi")) - (dom/pre (dom/text (pr-str (e/server ((e/partial-dynamic [hf/*$* hf/db] #(orders ""))))))) - (dom/pre (dom/text (pr-str (e/server (str hf/db))))) - (dom/pre (dom/text (pr-str (e/server (hfql [hf/*$* hf/db] 42))))) - (dom/pre (dom/text (pr-str (e/server (hfql [hf/*$* hf/db] :db/id 1))))) - (with-gridsheet-renderer - (e/server (hfql [hf/*$* hf/db] :db/id 1))))) - -#?(:clj - (tests - (alter-var-root #'hf/*$* (constantly model/*$*)) - (some? hf/*$*) := true - (orders "") := [1 2 3] - - (with (e/run (tap (binding [hf/db hf/*$* - hf/*nav!* model/nav!] - (hfql [] :db/id 1)))) - % := 1) - - (with (e/run (tap (binding [hf/db hf/*$* - hf/*nav!* model/nav!] - (hfql [] - {(orders "") - [:db/id - :order/email - :order/gender]})))) - % := {`(orders "") - [{:db/id 1, :order/email "alice@example.com", :order/gender :order/female} - {:db/id 2, :order/email "bob@example.com", :order/gender :order/male} - {:db/id 3, :order/email "charlie@example.com", :order/gender :order/male}]}))) diff --git a/src-fiddles/dustingetz/hfql_intro.md b/src-fiddles/dustingetz/hfql_intro.md deleted file mode 100644 index 3ee8fe5..0000000 --- a/src-fiddles/dustingetz/hfql_intro.md +++ /dev/null @@ -1,11 +0,0 @@ -# HFQL intro - -by Dustin Getz, 2023 July - wip - -!fiddle[](dustingetz.hfql-intro/Teeshirt-orders-1) - -!fiddle[](dustingetz.hfql-intro/Teeshirt-orders-2) - -!fiddle[](dustingetz.hfql-intro/Teeshirt-orders-3) - -* Fix router diff --git a/src-fiddles/dustingetz/hfql_teeshirt_orders.md b/src-fiddles/dustingetz/hfql_teeshirt_orders.md deleted file mode 100644 index e308dc3..0000000 --- a/src-fiddles/dustingetz/hfql_teeshirt_orders.md +++ /dev/null @@ -1,28 +0,0 @@ -# Teeshirt Orders – HFQL demo - -!fiddle-ns[](hfql-demo.hfql-teeshirt-orders/HFQL-teeshirt-orders) - - -What's happening -* there's a CRUD table, backed by a query -* the table is specified by 4 lines of HFQL + database schema + the clojure.spec for the query -* the filter input labeled `needle` is reflected from the clojure.spec on `orders`, which specifies that the `orders` query accepts a single `string?` parameter named `:needle` -* type "alice" into the input and see the query refresh live - -Novel forms -* `hf/hfql` -* `binding`: Electric dynamic scope, it's reactive and used for dependency injection -* `hf/db` -* `hf/*schema*` -* `hf/*nav!*` - -Key ideas -* HFQL's mission is to let you model CRUD apps in as few LOC as possible. -* HFQL generalizes graph-pull query notation into a declarative UI specification language. -* spec-driven UI -* macroexpands down to Electric -* network-transparent -* composes with Electric as e/defn -* scope - -Dependency injection with Electric bindings \ No newline at end of file diff --git a/src-fiddles/dustingetz/scratch.cljc b/src-fiddles/dustingetz/scratch.cljc deleted file mode 100644 index db6a53f..0000000 --- a/src-fiddles/dustingetz/scratch.cljc +++ /dev/null @@ -1,7 +0,0 @@ -(ns dustingetz.scratch - (:require [hyperfiddle.electric :as e] - [hyperfiddle.electric-dom2 :as dom])) - - -(e/defn Scratch [] - (e/client (dom/pre (dom/text "yo")))) diff --git a/src-fiddles/dustingetz/y_dir.cljc b/src-fiddles/dustingetz/y_dir.cljc deleted file mode 100644 index 20afa62..0000000 --- a/src-fiddles/dustingetz/y_dir.cljc +++ /dev/null @@ -1,47 +0,0 @@ -(ns dustingetz.y-dir - (:require [contrib.str :refer [includes-str?]] - [hyperfiddle.electric :as e] - [hyperfiddle.electric-dom2 :as dom] - [hyperfiddle.electric-ui4 :as ui] - dustingetz.y-fib)) - -#?(:clj (defn file-is-dir [h] (.isDirectory h))) -#?(:clj (defn file-is-file [h] (.isFile h))) -#?(:clj (defn file-list-files [h] (.listFiles h))) -#?(:clj (defn file-get-name [h] (.getName h))) -#?(:clj (defn file-absolute-path [^String path-str & more] - (-> (java.nio.file.Path/of ^String path-str (into-array String more)) - .toAbsolutePath str))) - -(e/defn Y [Gen] - (new (e/fn [F] (F. F)) - (e/fn F [F] - (Gen. (e/fn Recur [x] - (new (F. F) x)))))) - -(e/defn Dir-tree [Recur] - (e/server - (e/fn [[h s]] - (cond - (file-is-dir h) - (e/client - (dom/li (dom/text (e/server (file-get-name h))) - (dom/ul - (e/server - (e/for [x (file-list-files h)] - (Recur. [x s])))))) ; recur - - (file-is-file h) - (when (includes-str? (file-get-name h) s) - (let [name_ (e/server (file-get-name h))] - (e/client (dom/li (dom/text name_))))))))) - -(e/defn Y-dir [] - (e/client - (dom/div - (let [!s (atom "") s (e/watch !s)] - (ui/input s (e/fn [v] (reset! !s v))) - (dom/ul - (e/server - (let [h (clojure.java.io/file (file-absolute-path "./src"))] - (new (Y. Dir-tree) [h s])))))))) diff --git a/src-fiddles/dustingetz/y_fib.cljc b/src-fiddles/dustingetz/y_fib.cljc deleted file mode 100644 index b15ad82..0000000 --- a/src-fiddles/dustingetz/y_fib.cljc +++ /dev/null @@ -1,23 +0,0 @@ -(ns dustingetz.y-fib - (:require [hyperfiddle.electric :as e] - [hyperfiddle.electric-dom2 :as dom])) - -(e/defn Y [Gen] - (new (e/fn [F] (F. F)) ; call-with-self - (e/fn F [F] - (Gen. (e/fn Recur [x] - (new (F. F) x)))))) - -(e/defn Trace [x] - (e/client (dom/div (dom/text x))) - x) - -(e/defn Fib [Recur] - (e/fn [x] - (Trace. - (case x - 0 1 - (* x (Recur. (dec x))))))) - -(e/defn Y-fib [] - (new (Y. Fib) 15)) \ No newline at end of file diff --git a/src-triage/electric_demo/demo_10k_dom.cljc b/src-triage/electric_demo/demo_10k_dom.cljc deleted file mode 100644 index 5fb07be..0000000 --- a/src-triage/electric_demo/demo_10k_dom.cljc +++ /dev/null @@ -1,53 +0,0 @@ -(ns electric-demo.demo-10k-dom - (:require [hyperfiddle.electric :as e] - [hyperfiddle.electric-dom2 :as dom] - [missionary.core :as m])) - -(def !moves #?(:clj (atom []) :cljs nil)) -(def !board-size #?(:clj (atom 10000) :cljs nil)) -(e/def board-size (e/server (e/watch !board-size))) - -(comment (do (reset! !moves []) (reset! !board-size 2000))) - -#?(:cljs - (defn hot [el] - (m/observe (fn mount [!] - (dom/set-property! el "style" {:background-color "red"}) - (! nil) ; initial value - (fn unmount [] - (dom/set-property! el "style" {:background-color nil})))))) - -(e/defn Dom-10k-Elements [] - (e/client - (dom/h1 (dom/text "10k dom elements (multiplayer)")) - ; fixed width font + inline-block optimizes browser layout - (dom/element "style" (dom/text ".board div { width: 1em; height: 1em; display: inline-block; border: 1px #eee solid; }")) - (dom/element "style" (dom/text ".board { font-family: monospace; font-size: 7px; margin: 0; padding: 0; line-height: 0; }")) - (dom/div {:class "board"} - (e/for [i (range 0 board-size)] - (dom/div - (dom/on "mouseover" (e/fn [e] (e/server (swap! !moves conj i)))))) - (e/for [i (e/server (e/watch !moves))] - ; differential side effects, indexed by HTMLCollection - (new (hot (.item (.. dom/node -children) i))))))) - -;(defn countdown [x] ; Count down to 0 then terminate. -; (m/relieve {} (m/ap (loop [x x] -; (m/amb x -; (if (pos? x) -; (do (m/? (m/sleep 100)) -; (recur (dec x))) -; x)))))) -; -;(defn cell-color [x] -; (if (> x 1) ; In Electric-land, this conditional would introduce a switch and trigger a ws message for client-server frame coordination. -; (apply css-rgb-str (hsv->rgb (/ 0 360) -; (-> x (/ 7.5) (* 1.33)) -; 0.95)) -; "#eee")) -; -;#?(:cljs (defn x [!el] -; (m/observe (fn mount [!] -; (let [!o (js/MutationObserver !)] -; (.observe !o !el #js {"attributes" true}) -; (fn unmount [] (.disconnect !o))))))) \ No newline at end of file diff --git a/src-triage/electric_demo/demo_color.cljc b/src-triage/electric_demo/demo_color.cljc deleted file mode 100644 index 587d411..0000000 --- a/src-triage/electric_demo/demo_color.cljc +++ /dev/null @@ -1,106 +0,0 @@ -(ns electric-demo.demo-color - (:require [contrib.data :refer [assoc-vec]] - [hyperfiddle.electric :as e] - [hyperfiddle.electric-dom2 :as dom] - [hyperfiddle.history :as router] - [contrib.color :as c])) - -;; Goal is to demonstrate: -;; - fine-grained reactivity on CSS properties -;; - Non-trivial DOM api usage (canvas) - -(def CANVAS-WIDTH 360) ; px -(def CANVAS-HEIGHT 100) ; px - -(defn format-rgb [[r g b]] (str "rgb("r","g","b")")) - -#?(:cljs - (defn draw! [^js canvas colorf] - (let [ctx (.getContext canvas "2d")] - (loop [angle 0] - (set! (.-strokeStyle ctx) (colorf angle)) - (.beginPath ctx) - (.moveTo ctx angle 0) - (.lineTo ctx angle CANVAS-HEIGHT) - (.closePath ctx) - (.stroke ctx) - (when (< angle 360) - (recur (inc angle))))))) - -#?(:cljs - (defn draw-gradient! [canvas hue colorf] - (draw! canvas (fn [angle] (format-rgb (if (= angle hue) [255 255 255] (colorf angle))))))) - -(defn saturation->chroma [saturation] (* 0.158 (/ saturation 100))) - -(e/defn Tile [color] - (dom/div (dom/props {:style {:display :flex - :align-items :center - :justify-content :center - :color :white - :background-color (format-rgb color) - :width "100px" - :height "100%" - }}) - (dom/text "Contrast"))) - -(e/defn Color [] - (e/client - (let [[self h s l] router/route - h (or h 180) - s (or s 80) - l (or l 70) - swap-route! router/swap-route!] - (dom/div (dom/props {:style {:display :grid - :grid-template-columns "auto 1fr auto" - :gap "0 1rem" - :align-items :center - :justify-items :stretch - :max-width "600px"}}) - (dom/p (dom/text "Lightness")) - (dom/input (dom/props {:type :range - :min 0 - :max 100 - :step 1 - :value l}) - (dom/on! "input" (fn [^js e] (swap-route! assoc-vec 3 (js/parseInt (.. e -target -value)))))) - (dom/p (dom/text l "%")) - - (dom/p (dom/text "Saturation")) - (dom/input (dom/props {:type :range - :min 0 - :max 100 - :step 1 - :value s}) - (dom/on! "input" (fn [^js e] (swap-route! assoc-vec 2 (js/parseInt (.. e -target -value)))))) - (dom/p (dom/text s "%")) - - - (dom/p (dom/text "Hue")) - (dom/input (dom/props {:type :range - :min 0 - :max 360 - :step 1 - :value h}) - (dom/on! "input" (fn [^js e] (swap-route! assoc-vec 1 (js/parseInt (.. e -target -value)))))) - (dom/p (dom/text h "°")) - - - (dom/p (dom/text "HSL")) - (dom/canvas (dom/props {:width 360 - :height 100}) - (draw-gradient! dom/node h (fn [h] (c/hsl->rgb [h s l]))) - ) - (Tile. (c/hsl->rgb [h s l])) - - (dom/p (dom/text "OKLCH")) - (dom/canvas (dom/props {:width 360 - :height 100}) - (draw-gradient! dom/node h (fn [h] (c/oklch->rgb [l (saturation->chroma s) h])))) - (Tile. (c/oklch->rgb [l (saturation->chroma s) h])) - - (dom/p (dom/text "HSLuv")) - (dom/canvas (dom/props {:width 360 - :height 100}) - (draw-gradient! dom/node h (fn [h] (c/hsluv->rgb [h s l])))) - (Tile. (c/hsluv->rgb [h s l])))))) diff --git a/src-triage/electric_demo/demo_explorer.cljc b/src-triage/electric_demo/demo_explorer.cljc deleted file mode 100644 index 3f5e337..0000000 --- a/src-triage/electric_demo/demo_explorer.cljc +++ /dev/null @@ -1,60 +0,0 @@ -(ns electric-demo.demo-explorer - (:require [clojure.datafy :refer [datafy]] - [clojure.core.protocols :refer [nav]] - #?(:clj clojure.java.io) - [contrib.data :refer [treelister]] - [contrib.datafy-fs #?(:clj :as :cljs :as-alias) fs] - contrib.str - [hyperfiddle.electric :as e] - [hyperfiddle.electric-dom2 :as dom] - [hyperfiddle.history :as router] - [contrib.gridsheet :as gridsheet :refer [Explorer]])) - -(def unicode-folder "\uD83D\uDCC2") ; 📂 - -(e/defn Render-cell [m a] - (let [v (a m)] - (case a - ::fs/name (case (::fs/kind m) - ::fs/dir (let [absolute-path (::fs/absolute-path m)] - (e/client (router/link absolute-path (dom/text v)))) - (::fs/other ::fs/symlink ::fs/unknown-kind) (e/client (dom/text v)) - (e/client (dom/text v))) - ::fs/modified (e/client (some-> v .toLocaleDateString dom/text)) - ::fs/kind (case (::fs/kind m) - ::fs/dir (e/client (dom/text unicode-folder)) - (e/client (some-> v name dom/text))) - (e/client (dom/text (str v)))))) - -(e/defn Dir [x] - (let [m (datafy x) - xs (nav m ::fs/children (::fs/children m))] - (e/client (dom/h1 (dom/text (e/server (::fs/absolute-path m))))) - (Explorer. - (treelister xs ::fs/children #(contrib.str/includes-str? (::fs/name %) %2)) - {::dom/style {:height "calc((20 + 1) * 24px)"} - ::gridsheet/page-size 20 - ::gridsheet/row-height 24 - ::gridsheet/Format Render-cell - ::gridsheet/columns [::fs/name ::fs/modified ::fs/size ::fs/kind] - ::gridsheet/grid-template-columns "auto 8em 5em 3em"}))) - -(e/defn DirectoryExplorer [] - (e/client - (dom/link (dom/props {:rel :stylesheet, :href "user/gridsheet-optional.css"})) - (dom/div (dom/props {:class "user-gridsheet-demo"}) - (binding [router/build-route (fn [[self state local-route] local-route'] - ; root local links through this entrypoint - `[DirectoryExplorer ~state ~local-route'])] - (e/server - (let [[self s route] (e/client router/route) - fs-path (or route (fs/absolute-path "./"))] - (e/client - (router/router 1 ; focus state slot, todo: fix IndexOutOfBounds exception - (e/server - (Dir. (clojure.java.io/file fs-path))))))))))) - -; Improvements -; Native search -; lazy folding/unfolding directories (no need for pagination) -; forms (currently table hardcoded with recursive pull) diff --git a/src-triage/electric_demo/demo_tic_tac_toe.cljc b/src-triage/electric_demo/demo_tic_tac_toe.cljc deleted file mode 100644 index 5dd89f1..0000000 --- a/src-triage/electric_demo/demo_tic_tac_toe.cljc +++ /dev/null @@ -1,31 +0,0 @@ -(ns electric-demo.demo-tic-tac-toe - (:require [hyperfiddle.electric :as e] - [hyperfiddle.electric-dom2 :as dom] - [hyperfiddle.electric-ui4 :as ui])) - -(def !x #?(:clj (atom (vec (repeat 10 0))) :cljs nil)) -(e/def x (e/server (e/watch !x))) -(defn update-board [board pos] (update board pos #(case % 0 1, 1 2, 2 0))) - -(e/defn Button [offset] - (ui/button (e/fn [] - (e/server - (swap! !x update-board offset))) - (dom/text - (case (e/server (nth x offset)) - 2 "x" - 1 "o" - 0 "-")))) - -(e/defn TicTacToe [] - (e/client - (dom/h1 (dom/text "Tic Tac Toe \uD83C\uDFAE")) - (dom/p (dom/text "multiplayer works, try two tabs")) - (dom/table - (e/for [row [[0 1 2] - [3 4 5] - [6 7 8]]] - (dom/tr - (e/for [i row] - (dom/td - (Button. i)))))))) diff --git a/src-triage/electric_demo/demo_todomvc.cljc b/src-triage/electric_demo/demo_todomvc.cljc deleted file mode 100644 index cd322b0..0000000 --- a/src-triage/electric_demo/demo_todomvc.cljc +++ /dev/null @@ -1,226 +0,0 @@ -(ns electric-demo.demo-todomvc - "Requires -Xss2m to compile. The Electric compiler exceeds the default 1m JVM ThreadStackSize - due to large macroexpansion resulting in false StackOverflowError during analysis." - (:require - contrib.str - #?(:clj [datomic.api :as d]) - [hyperfiddle.electric :as e] - [hyperfiddle.electric-dom2 :as dom] - [hyperfiddle.electric-ui4 :as ui] - [missionary.core :as m])) - -;;; Datomic plumbing -#?(:clj - (defn next-db< [conn] - (let [q (d/tx-report-queue conn)] - (m/observe (fn [!] - (! (d/db conn)) - (let [t (Thread. ^Runnable - #(when (try (! (:db-after (.take ^java.util.concurrent.LinkedBlockingQueue q))) - true - (catch InterruptedException _)) - (recur)))] - (.setDaemon t true) - (.start t) - #(doto t .interrupt .join))))))) - -;; Datomic only allows a single queue consumer, so we need to spawn a singleton here -;; In the next Electric iteration we can use `m/signal` and clean this up -#?(:clj (defonce !db (atom nil))) -#?(:clj (defonce !taker nil)) -#?(:clj (defn init-conn [schema] - (let [uri "datomic:mem://todomvc"] - (d/delete-database uri) - (d/create-database uri) - (let [conn (d/connect uri)] - (d/transact conn schema) - (when !taker (!taker)) - (alter-var-root #'!taker (fn [_] ((m/reduce #(reset! !db %2) nil (next-db< conn)) identity identity))) - conn)))) - -;; Application -#?(:clj - (def schema - [{:db/ident :task/status, :db/valueType :db.type/keyword, :db/cardinality :db.cardinality/one} - {:db/ident :task/description, :db/valueType :db.type/string, :db/cardinality :db.cardinality/one}])) - - -#?(:clj (defonce !conn (init-conn schema))) -#?(:clj (comment (alter-var-root #'!conn (fn [_] (init-conn schema))))) - -(e/def db) ; server -(e/def transact!) ; server -(def !state #?(:cljs (atom {::filter :all ; client - ::editing nil - ::delay 0}))) - -#?(:clj - (defn query-todos [db filter] - {:pre [filter]} - (case filter - :active (d/q '[:find [?e ...] :where [?e :task/status :active]] db) - :done (d/q '[:find [?e ...] :where [?e :task/status :done]] db) - :all (d/q '[:find [?e ...] :where [?e :task/status]] db)))) - -#?(:clj - (defn todo-count [db filter] - {:pre [filter] - :post [(number? %)]} - (-> (case filter - :active (d/q '[:find (count ?e) . :where [?e :task/status :active]] db) - :done (d/q '[:find (count ?e) . :where [?e :task/status :done]] db) - :all (d/q '[:find (count ?e) . :where [?e :task/status]] db)) - (or 0)))) ; datascript can return nil wtf - -(e/defn Filter-control [state target label] - (dom/a (dom/props {:class (when (= state target) "selected")}) - (dom/text label) - (dom/on "click" (e/fn [_] (swap! !state assoc ::filter target))))) - - -(e/defn TodoStats [state] - (let [active (e/server (todo-count db :active)) - done (e/server (todo-count db :done))] - (dom/div - (dom/span (dom/props {:class "todo-count"}) - (dom/strong (dom/text active)) - (dom/span (dom/text " " (str (case active 1 "item" "items")) " left"))) - - (dom/ul (dom/props {:class "filters"}) - (dom/li (Filter-control. (::filter state) :all "All")) - (dom/li (Filter-control. (::filter state) :active "Active")) - (dom/li (Filter-control. (::filter state) :done "Completed"))) - - (when (pos? done) - (ui/button (e/fn [] (e/server (when-some [ids (seq (query-todos db :done))] - (transact! (mapv (fn [id] [:db/retractEntity id]) ids)) nil))) - (dom/props {:class "clear-completed"}) - (dom/text "Clear completed " done)))))) - -(e/defn TodoItem [state id] - (e/server - ;; we'd use `d/entity` is not for this Datomic bug - ;; https://ask.datomic.com/index.php/859/equality-on-d-entity-ignores-db?show=859#q859 - (let [{:keys [:task/status :task/description]} (d/pull db '[:task/status :task/description] id)] - (e/client - (let [uuid (random-uuid)] - (dom/li - (dom/props {:class [(when (= :done status) "completed") - (when (= uuid (::editing state)) "editing")]}) - (dom/div (dom/props {:class "view"}) - (ui/checkbox (= :done status) (e/fn [v] - (let [status (case v true :done, false :active, nil)] - (e/server (transact! [{:db/id id, :task/status status}]) nil))) - (dom/props {:class "toggle"})) - (dom/label (dom/text description) - (dom/on "dblclick" (e/fn [_] (swap! !state assoc ::editing uuid))))) - (when (= uuid (::editing state)) - (dom/span (dom/props {:class "input-load-mask"}) - (dom/on-pending (dom/props {:aria-busy true}) - (dom/input - (dom/on "keydown" - (e/fn [e] - (case (.-key e) - "Enter" (when-some [description (contrib.str/blank->nil (-> e .-target .-value))] - (case (e/server (transact! [{:db/id id, :task/description description}]) nil) - (swap! !state assoc ::editing nil))) - "Escape" (swap! !state assoc ::editing nil) - nil))) - (dom/on "blur" - (e/fn [e] - (when-some [description (contrib.str/blank->nil (-> e .-target .-value))] - (case (e/server (transact! [{:db/id id, :task/description description}]) nil) - (swap! !state assoc ::editing nil))))) - (dom/props {:class "edit" #_#_:autofocus true}) - (dom/bind-value description) ; first set the initial value, then focus - (case description ; HACK sequence - run focus after description is available - (.focus dom/node)))))) - (ui/button (e/fn [] (e/server (transact! [[:db/retractEntity id]]) nil)) - (dom/props {:class "destroy"})))))))) - -#?(:clj - (defn toggle-all! [db status] - (let [ids (query-todos db (if (= :done status) :active :done))] - (map (fn [id] {:db/id id, :task/status status}) ids)))) - -(e/defn TodoList [state] - (e/client - (dom/div - (dom/section (dom/props {:class "main"}) - (let [active (e/server (todo-count db :active)) - all (e/server (todo-count db :all)) - done (e/server (todo-count db :done))] - (ui/checkbox (cond (= all done) true - (= all active) false - :else nil) - (e/fn [v] (let [status (case v (true nil) :done, false :active)] - (e/server (transact! (toggle-all! db status)) nil))) - (dom/props {:class "toggle-all"}))) - (dom/label (dom/props {:for "toggle-all"}) (dom/text "Mark all as complete")) - (dom/ul (dom/props {:class "todo-list"}) - (e/for [id (e/server (sort (query-todos db (::filter state))))] - (TodoItem. state id))))))) - -(e/defn CreateTodo [] - (dom/span (dom/props {:class "input-load-mask"}) - (dom/on-pending (dom/props {:aria-busy true}) - (dom/input - (ui/on-submit (e/fn [description] - (e/server (transact! [{:task/description description, :task/status :active}]) nil))) - (dom/props {:class "new-todo", :placeholder "What needs to be done?"}))))) - -(e/defn TodoMVC-UI [state] - (dom/section (dom/props {:class "todoapp"}) - (dom/header (dom/props {:class "header"}) - (CreateTodo.)) - (when (e/server (pos? (todo-count db :all))) - (TodoList. state)) - (dom/footer (dom/props {:class "footer"}) - (TodoStats. state)))) - -(e/defn TodoMVC-body [state] - (dom/div (dom/props {:class "todomvc"}) - (dom/h1 (dom/text "TodoMVC")) - (TodoMVC-UI. state) - (dom/footer (dom/props {:class "info"}) - (dom/p (dom/text "Double-click to edit a todo"))))) - -(e/defn Diagnostics [state] - (dom/h1 (dom/text "Diagnostics")) - (dom/dl - (dom/dt (dom/text "count :all")) (dom/dd (dom/text (pr-str (e/server (todo-count db :all))))) - (dom/dt (dom/text "query :all")) (dom/dd (dom/text (pr-str (e/server (query-todos db :all))))) - (dom/dt (dom/text "state")) (dom/dd (dom/text (pr-str state))) - (dom/dt (dom/text "delay")) (dom/dd - (ui/long (::delay state) (e/fn [v] (swap! !state assoc ::delay v)) - (dom/props {:step 1, :min 0, :style {:width :min-content}})) - (dom/text " ms")))) - -#?(:clj - (defn slow-transact! [!conn delay tx] - (try (Thread/sleep delay) ; artificial latency - (d/transact !conn tx) - (catch InterruptedException _)))) - -(e/defn TodoMVC [] - (e/client - (let [state (e/watch !state)] - (e/server - (binding [db (e/watch !db) - transact! (partial slow-transact! !conn (e/client (::delay state)))] - (e/client - (dom/link (dom/props {:rel :stylesheet, :href "/todomvc.css"})) - ; exclude #root style from todomvc-composed by inlining here - (dom/element "style" (dom/text "body.hyperfiddle { width: 65vw; margin-left: auto; margin-right: auto; }")) - (TodoMVC-body. state) - #_(Diagnostics. state))))))) - -(comment - (todo-count @!conn :all) - (todo-count @!conn :active) - (todo-count @!conn :done) - (query-todos @!conn :all) - (query-todos @!conn :active) - (query-todos @!conn :done) - (d/q '[:find (count ?e) . :where [?e :task/status]] @!conn) - ) diff --git a/src-triage/electric_demo/demo_todomvc_branched.cljc b/src-triage/electric_demo/demo_todomvc_branched.cljc deleted file mode 100644 index 1f0d5c3..0000000 --- a/src-triage/electric_demo/demo_todomvc_branched.cljc +++ /dev/null @@ -1,237 +0,0 @@ -(ns electric-demo.demo-todomvc-branched - "Requires -Xss2m to compile. The Electric compiler exceeds the default 1m JVM ThreadStackSize - due to large macroexpansion resulting in false StackOverflowError during analysis." - (:require - #?(:clj [contrib.datomic-contrib :as dx]) - #?(:clj [datomic.api :as d]) - [contrib.debug :as dbg] - contrib.str - [hyperfiddle.api :as hf] - [hyperfiddle.electric :as e] - [hyperfiddle.electric-dom2 :as dom] - [hyperfiddle.electric-ui4 :as ui] - [missionary.core :as m])) - -;;; Datomic plumbing -#?(:clj - (defn next-db< [conn] - (let [q (d/tx-report-queue conn)] - (m/observe (fn [!] - (! (d/db conn)) - (let [t (Thread. ^Runnable - #(when (try (! (:db-after (.take ^java.util.concurrent.LinkedBlockingQueue q))) - true - (catch InterruptedException _)) - (recur)))] - (.setDaemon t true) - (.start t) - #(doto t .interrupt .join))))))) - -;; Datomic only allows a single queue consumer, so we need to spawn a singleton here -;; In the next Electric iteration we can use `m/signal` and clean this up -#?(:clj (defonce !db (atom nil))) -#?(:clj (defonce !taker nil)) -#?(:clj (defn init-conn [schema] - (let [uri "datomic:mem://todomvc-branched"] - (d/delete-database uri) - (d/create-database uri) - (let [conn (d/connect uri)] - (d/transact conn schema) - (when !taker (!taker)) - (alter-var-root #'!taker (fn [_] ((m/reduce #(reset! !db %2) nil (next-db< conn)) identity identity))) - conn)))) - -;; Application -#?(:clj - (def schema - [{:db/ident :task/status, :db/valueType :db.type/keyword, :db/cardinality :db.cardinality/one} - {:db/ident :task/description, :db/valueType :db.type/string, :db/cardinality :db.cardinality/one}])) - - -#?(:clj (defonce !conn (init-conn schema))) -#?(:clj (comment (alter-var-root #'!conn (fn [_] (init-conn schema))))) - -(e/def db) ; server -(def !state #?(:cljs (atom {::filter :all ; client - ::editing nil - ::delay 0}))) - -#?(:clj - (defn query-todos [db filter] - {:pre [filter]} - (case filter - :active (d/q '[:find [?e ...] :where [?e :task/status :active]] db) - :done (d/q '[:find [?e ...] :where [?e :task/status :done]] db) - :all (d/q '[:find [?e ...] :where [?e :task/status]] db)))) - -#?(:clj - (defn todo-count [db filter] - {:pre [filter] - :post [(number? %)]} - (-> (case filter - :active (d/q '[:find (count ?e) . :where [?e :task/status :active]] db) - :done (d/q '[:find (count ?e) . :where [?e :task/status :done]] db) - :all (d/q '[:find (count ?e) . :where [?e :task/status]] db)) - (or 0)))) ; datascript can return nil wtf - -(e/defn Filter-control [state target label] - (dom/a (dom/props {:class (when (= state target) "selected")}) - (dom/text label) - (dom/on "click" (e/fn [_] (swap! !state assoc ::filter target))))) - - -(e/defn TodoStats [state] - (let [active (e/server (todo-count hf/db :active)) - done (e/server (todo-count hf/db :done))] - (dom/div - (dom/span (dom/props {:class "todo-count"}) - (dom/strong (dom/text active)) - (dom/span (dom/text " " (str (case active 1 "item" "items")) " left"))) - - (dom/ul (dom/props {:class "filters"}) - (dom/li (Filter-control. (::filter state) :all "All")) - (dom/li (Filter-control. (::filter state) :active "Active")) - (dom/li (Filter-control. (::filter state) :done "Completed"))) - - (when (pos? done) - (ui/button (e/fn [] (e/server (when-some [ids (seq (query-todos hf/db :done))] - (hf/Transact!. (mapv (fn [id] [:db/retractEntity id]) ids)) nil))) - (dom/props {:class "clear-completed"}) - (dom/text "Clear completed " done)))))) - -(e/defn TodoItem [state id] - (e/server - ;; we'd use `d/entity` is not for this Datomic bug - ;; https://ask.datomic.com/index.php/859/equality-on-d-entity-ignores-db?show=859#q859 - (let [{:keys [:task/status :task/description]} (d/pull hf/db '[:task/status :task/description] id)] - (e/client - (dom/li - (dom/props {:class [(when (= :done status) "completed") - (when (= id (::editing state)) "editing")]}) - (dom/div (dom/props {:class "view"}) - (ui/checkbox (= :done status) (e/fn [v] - (let [status (case v true :done, false :active, nil)] - (e/server (hf/Transact!. [{:db/id id, :task/status status}]) nil))) - (dom/props {:class "toggle"})) - (dom/label (dom/text description) - (dom/on "dblclick" (e/fn [_] (swap! !state assoc ::editing id))))) - (when (= id (::editing state)) - (dom/span (dom/props {:class "input-load-mask"}) - (dom/on-pending (dom/props {:aria-busy true}) - (dom/input - (dom/on "keydown" - (e/fn [e] - (case (.-key e) - "Enter" (when-some [description (contrib.str/blank->nil (-> e .-target .-value))] - (case (e/server (hf/Transact!. [{:db/id id, :task/description description}]) nil) - (swap! !state assoc ::editing nil))) - "Escape" (swap! !state assoc ::editing nil) - nil))) - (dom/on "blur" - (e/fn [e] - (when-some [description (contrib.str/blank->nil (-> e .-target .-value))] - (case (e/server (hf/Transact!. [{:db/id id, :task/description description}]) nil) - (swap! !state assoc ::editing nil))))) - (dom/props {:class "edit" #_#_:autofocus true}) - (dom/bind-value description) ; first set the initial value, then focus - (case description ; HACK sequence - run focus after description is available - (.focus dom/node)))))) - (ui/button (e/fn [] (e/server (hf/Transact!. [[:db/retractEntity id]]) nil)) - (dom/props {:class "destroy"}))))))) - -#?(:clj - (defn toggle-all! [db status] - (let [ids (query-todos db (if (= :done status) :active :done))] - (map (fn [id] {:db/id id, :task/status status}) ids)))) - -(e/defn TodoList [state] - (e/client - (dom/div - (dom/section (dom/props {:class "main"}) - (let [active (e/server (todo-count hf/db :active)) - all (e/server (todo-count hf/db :all)) - done (e/server (todo-count hf/db :done))] - (ui/checkbox (cond (= all done) true - (= all active) false - :else nil) - (e/fn [v] (let [status (case v (true nil) :done, false :active)] - (e/server (hf/Transact!. (toggle-all! hf/db status)) nil))) - (dom/props {:class "toggle-all"}))) - (dom/label (dom/props {:for "toggle-all"}) (dom/text "Mark all as complete")) - (dom/ul (dom/props {:class "todo-list"}) - (e/for [id (e/server (sort (query-todos hf/db (::filter state))))] - (TodoItem. state id))))))) - -(e/defn CreateTodo [] - (dom/span (dom/props {:class "input-load-mask"}) - (dom/on-pending (dom/props {:aria-busy true}) - (dom/input - (ui/on-submit (e/fn [description] - (e/server (hf/Transact!. [{:task/description description, :task/status :active}]) nil))) - (dom/props {:class "new-todo", :placeholder "What needs to be done?"}))))) - -(e/defn TodoMVC-UI [state] - (dom/section (dom/props {:class "todoapp"}) - (dom/header (dom/props {:class "header"}) - (CreateTodo.)) - (when (e/server (pos? (todo-count hf/db :all))) - (TodoList. state)) - (dom/footer (dom/props {:class "footer"}) - (TodoStats. state)))) - -(e/defn TodoMVC-body [state] - (dom/div (dom/props {:class "todomvc"}) - (dom/h1 (dom/text "TodoMVC")) - (TodoMVC-UI. state) - (dom/footer (dom/props {:class "info"}) - (dom/p (dom/text "Double-click to edit a todo"))))) - -(e/defn Diagnostics [state] - (dom/h1 (dom/text "Diagnostics")) - (dom/dl - (dom/dt (dom/text "count :all")) (dom/dd (dom/text (pr-str (e/server (todo-count hf/db :all))))) - (dom/dt (dom/text "query :all")) (dom/dd (dom/text (pr-str (e/server (query-todos hf/db :all))))) - (dom/dt (dom/text "state")) (dom/dd (dom/text (pr-str state))) - (dom/dt (dom/text "delay")) (dom/dd - (ui/long (::delay state) (e/fn [v] (swap! !state assoc ::delay v)) - (dom/props {:step 1, :min 0, :style {:width :min-content}})) - (dom/text " ms")))) - -(e/defn TodoMVCBranched [] - (e/client - (let [state (e/watch !state)] - (e/server - (binding [db (e/watch !db) - hf/Transact! (e/fn [tx] (d/transact !conn tx))] - (binding [hf/schema (new (dx/schema> db)) - hf/into-tx' hf/into-tx - hf/with (fn [db tx] - (try (:db-after (d/with db tx)) - (catch Throwable ex (prn ex) #_(prn [(type ex) (ex-message ex)]) db))) - hf/db db] - (let [TransactInParent! hf/Transact!] - (hf/branch - (e/client - (dom/link (dom/props {:rel :stylesheet, :href "/todomvc.css"})) - ; exclude #root style from todomvc-composed by inlining here - (dom/element "style" (dom/text "body.hyperfiddle { width: 65vw; margin-left: auto; margin-right: auto; }")) - (TodoMVC-body. state) - #_ (Diagnostics. state) - (dom/hr) - (ui/button-colored (e/fn [] (e/server (TransactInParent!. hf/stage) nil)) - (dom/text "Commit") - (dom/props {:disabled (e/server (empty? hf/stage))})) - (ui/button-colored (e/fn [] (e/server (hf/ClearStage!.) nil)) - (dom/text "Discard") - (dom/props {:disabled (e/server (empty? hf/stage))})) - (ui/edn (e/server hf/stage) nil (dom/props {:disabled true}))))))))))) - -(comment - (todo-count (d/db !conn) :all) - (todo-count @!conn :active) - (todo-count @!conn :done) - (query-todos @!conn :all) - (query-todos @!conn :active) - (query-todos @!conn :done) - (d/q '[:find (count ?e) . :where [?e :task/status]] @!conn) - ) diff --git a/src-triage/electric_demo/demo_todomvc_composed.cljc b/src-triage/electric_demo/demo_todomvc_composed.cljc deleted file mode 100644 index fff1ff2..0000000 --- a/src-triage/electric_demo/demo_todomvc_composed.cljc +++ /dev/null @@ -1,38 +0,0 @@ -(ns electric-demo.demo-todomvc-composed - (:require - #?(:clj [datomic.api :as d]) - [hyperfiddle.electric :as e] - [hyperfiddle.electric-dom2 :as dom] - [hyperfiddle.electric-ui4 :as ui] - [electric-demo.demo-todomvc :as todomvc])) - -(def !n #?(:clj (atom 1))) - -(e/defn PopoverCascaded [i F] - (let [!focused (atom false) focused (e/watch !focused)] - (dom/div (dom/props {:style {:position "absolute" - :width "50vw" - :left (str (* i 40) "px") - :top (str (-> i (* 40) (+ 60)) "px") - :z-index (+ i (if focused 1000 0))}}) - (dom/on "mouseenter" (e/fn [_] (reset! !focused true))) - (dom/on "mouseleave" (e/fn [_] (reset! !focused false))) - (F.)))) - -(e/defn TodoMVC-composed [] - (e/client - (let [state (e/watch todomvc/!state) - n (e/server (e/watch !n))] - (e/server - (binding [todomvc/db (e/watch todomvc/!db) - todomvc/transact! (partial d/transact todomvc/!conn)] - (e/client - (dom/link (dom/props {:rel :stylesheet, :href "/todomvc.css"})) - (ui/range n (e/fn [v] (e/server (reset! !n v))) - (dom/props {:min 1 :max 25 :step 1})) - (dom/div (dom/props {:class "todomvc" :style {:position "relative"}}) - (dom/h1 (dom/text "TodoMVC")) - - (e/for [i (range n)] ; <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< - (PopoverCascaded. i - (e/fn [] (todomvc/TodoMVC-UI. state))))))))))) diff --git a/src-triage/electric_demo/demo_virtual_scroll.cljc b/src-triage/electric_demo/demo_virtual_scroll.cljc deleted file mode 100644 index 1551c6a..0000000 --- a/src-triage/electric_demo/demo_virtual_scroll.cljc +++ /dev/null @@ -1,76 +0,0 @@ -(ns electric-demo.demo-virtual-scroll - "Server-streamed virtual pagination over node_modules. Check the DOM!" - (:require [contrib.data :refer [unqualify]] - [hyperfiddle.electric :as e] - [hyperfiddle.electric-dom2 :as dom] - [hyperfiddle.electric-ui4 :as ui] - #?(:cljs goog.object))) - -(e/defn DemoFixedHeightCounted - "Scrolls like google sheets. this can efficiently jump through a large indexed collection" - [] - (let [row-count 500 - xs (vec (range row-count)) ; counted - page-size 100 - row-height 22] ; todo use relative measurement (browser zoom impacts px height) - (e/client - (dom/div (dom/props {:class "viewport" :style {:overflowX "hidden" :overflowY "auto"}}) - (let [[scrollTop] (new (ui/scroll-state< dom/node)) - max-height (* row-count row-height) - clamped-scroll-top (js/Math.min scrollTop max-height) - start (/ clamped-scroll-top row-height)] ; (js/Math.floor) - (dom/div (dom/props {:style {:height (str (* row-height row-count) "px") ; optional absolute scrollbar - :padding-top (str clamped-scroll-top "px") ; seen elements are replaced with padding - :padding-bottom (str (- max-height clamped-scroll-top) "px")}}) - (e/server - ; seen elements are unmounted - (e/for [x #_(subvec xs - (Math/min start row-count) - (Math/min (+ start page-size) row-count)) - (->> xs (drop start) (take page-size))] - (e/client (dom/div (dom/text x))))))))))) - -(e/defn DemoVariableHeightInfinite - "Scrolls like newsfeed. Natural browser layout for variable height rows. Leaves seen elements - mounted in the DOM." - [] - (let [xs (range) ; infinite - page-size 100] - (e/client - (dom/div (dom/props {:class "viewport"}) - (let [!pages (atom 1) pages (e/watch !pages) - [scrollTop scrollHeight clientHeight] (new (ui/scroll-state< dom/node))] - (when (>= scrollTop (- scrollHeight clientHeight clientHeight)) ; scrollThresholdPx = clientHeight - (swap! !pages inc)) ; can this get spammed by Electric? - (dom/div ; content is unstyled, uses natural layout - (e/server - (e/for [x (->> xs (take (* pages page-size)))] ; leave dom - (e/client (dom/div (dom/text x))))))))))) - -#?(:clj (defonce !demo (atom {:text "DemoFixedHeightCounted" ::value `DemoFixedHeightCounted}))) - -(e/def demo (e/server (e/watch !demo))) - -(e/def demos {`DemoVariableHeightInfinite DemoVariableHeightInfinite - `DemoFixedHeightCounted DemoFixedHeightCounted}) - -(e/defn VirtualScroll [] - (e/client - ; Requires css {box-sizing: border-box;} - (dom/element "style" (dom/text ".header { position: fixed; z-index:1; top: 0; left: 0; right: 0; height: 100px; background-color: #abcdef; }" - ".footer { position: fixed; bottom: 0; left: 0; right: 0; height: 100px; background-color: #abcdef; }" - ".viewport { position: fixed; top: 100px; bottom: 100px; left: 0; right: 0; background-color: #F63; overflow: auto; }")) - (dom/div (dom/props {:class "header"}) - (dom/dl - (dom/dt (dom/text "scroll debug state")) - (dom/dd (dom/pre (dom/text (pr-str (update-keys (e/watch ui/!scrollStateDebug) unqualify)))))) - (e/server - (ui/select - demo - (e/fn V! [v] (reset! !demo v)) - (e/fn Options [] [{:text "DemoFixedHeightCounted" ::value `DemoFixedHeightCounted} - {:text "DemoVariableHeightInfinite" ::value `DemoVariableHeightInfinite}]) - (e/fn OptionLabel [x] (:text x))))) - (e/server (new (get demos (::value demo)))) - (dom/div (dom/props {:class "footer"}) - (dom/text "Try scrolling to the top, and resizing the window.")))) diff --git a/src-triage/electric_demo/wip/button_with_progress.cljc b/src-triage/electric_demo/wip/button_with_progress.cljc deleted file mode 100644 index 5f3bcde..0000000 --- a/src-triage/electric_demo/wip/button_with_progress.cljc +++ /dev/null @@ -1,23 +0,0 @@ -(ns electric-demo.wip.button-with-progress - (:require - [hyperfiddle.electric :as e] - [hyperfiddle.electric-dom2 :as dom] - [hyperfiddle.electric-ui4 :as ui] - [missionary.core :as m])) - -(e/defn Download! [!progress round] - (reset! !progress (* 20 round)) - (when (< round 5) - (let [ms (+ 100 (rand-int 500))] - (case (new (e/task->cp (m/sleep ms ms))) - (recur !progress (inc round)))))) - -(e/defn App [] - (let [!progress (atom 0), progress (e/watch !progress)] - (e/client - (dom/div (dom/style {:display "flex", :align-items "center", :gap "1rem"}) - (ui/button-colored (e/fn [] (e/server (new Download! !progress 0))) - (dom/text "Download")) - (dom/div (dom/style {:border "1px solid gray", :border-radius "0.4rem" - :overflow "hidden", :width "5rem", :height "1rem"}) - (dom/div (dom/style {:width (str progress "%"), :height "100%", :background-color "green"}))))))) diff --git a/src-triage/electric_demo/wip/demo_branched_route.cljc b/src-triage/electric_demo/wip/demo_branched_route.cljc deleted file mode 100644 index 579a5a0..0000000 --- a/src-triage/electric_demo/wip/demo_branched_route.cljc +++ /dev/null @@ -1,54 +0,0 @@ -(ns wip.demo-branched-route - (:require datascript.core - #?(:clj user.example-datascript-db) - [hyperfiddle.api :as hf] - [hyperfiddle.hfql-tree-grid :as ttgui] - [hyperfiddle.electric :as e] - [hyperfiddle.electric-dom2 :as dom] - hyperfiddle.popover - [hyperfiddle.history :as router] - wip.orders-datascript)) - -(e/def Page) -(e/defn Page-impl [] - (dom/h1 (dom/text "Branched route")) - (dom/pre (dom/text (contrib.str/pprint-str router/route))) - - (router/router :hfql - (e/server - (binding [hf/*nav!* wip.orders-datascript/nav! - hf/*schema* wip.orders-datascript/schema - hf/db hf/*$*] - (ttgui/with-gridsheet-renderer - (binding [ttgui/grid-width 2 - hf/db-name "$"] - (e/server - (hf/hfql {(wip.orders-datascript/orders .) [:db/id]}))))))) - - (dom/hr) - (router/router :left (hyperfiddle.popover/popover "Recur Left" (Page.))) - (router/router :right (hyperfiddle.popover/popover "Recur Right" (Page.)))) - -(e/defn RecursiveRouter [] - (hf/branch - (e/client - (binding [Page Page-impl] - (router/router 1 ; ordinal to nominal - representation only - (Page.)))))) - -(comment - (e/for [[page nested] s] - (router/router page (hyperfiddle.popover/popover "Recur Left" - (hf/eval-as-iframe nested)))) - - `(wip.demo-branched-route/RecursiveRouter - {::needle "root" - ::left {::needle "" - ::left {::needle "" - ::right {}}} - ::right {::needle ""}}) - - `(wip.demo-branched-route/RecursiveRouter - {::left `(wip.demo-branched-route/PDF) - ::right `(wip.demo-branched-route/HTML)}) - ) \ No newline at end of file diff --git a/src-triage/electric_demo/wip/demo_custom_types.cljc b/src-triage/electric_demo/wip/demo_custom_types.cljc deleted file mode 100644 index 2c63882..0000000 --- a/src-triage/electric_demo/wip/demo_custom_types.cljc +++ /dev/null @@ -1,29 +0,0 @@ -(ns electric-demo.wip.demo-custom-types - "Demo shows how to serialize custom types in Electric" - (:require [hyperfiddle.electric :as e] - [hyperfiddle.electric-dom2 :as dom] - [cognitect.transit :as t])) - -(defrecord MyCustomType [field]) ; custom type - -(def write-handler - (t/write-handler - (fn [_] "wip.demo-custom-types/MyCustomType") ; this tag must be namespaced! - (fn [x] (into {} x)))) - -(def read-handler (t/read-handler map->MyCustomType)) - -; Todo cleanup, there are better ways to do this -#?(:clj (alter-var-root #'hyperfiddle.electric.impl.io/*write-handlers* - assoc MyCustomType write-handler)) ; server: write only -#?(:cljs (set! hyperfiddle.electric.impl.io/*read-handlers* ; client: read only - (assoc hyperfiddle.electric.impl.io/*read-handlers* - "wip.demo-custom-types/MyCustomType" read-handler))) - -(e/defn CustomTypes [] - (e/server - (let [object (MyCustomType. "value")] - (e/client - (dom/dl - (dom/dt (dom/text "type")) (dom/dd (dom/text (pr-str (type object)))) - (dom/dt (dom/text "value")) (dom/dd (dom/text (pr-str object)))))))) diff --git a/src-triage/electric_demo/wip/demo_stage_ui4.cljc b/src-triage/electric_demo/wip/demo_stage_ui4.cljc deleted file mode 100644 index 21503eb..0000000 --- a/src-triage/electric_demo/wip/demo_stage_ui4.cljc +++ /dev/null @@ -1,112 +0,0 @@ -(ns electric-demo.wip.demo-stage-ui4 - "Database-backed CRUD form using Datomic. -Requires datomic mbrains database available, which is not in-mem so nontrivial to CI" - (:require [contrib.css :refer [css-slugify]] - [contrib.str :refer [pprint-str]] - #?(:clj [contrib.datomic-contrib :as dx]) - #?(:clj [datomic.api :as d]) - [hyperfiddle.api :as hf] - [hyperfiddle.electric :as e] - [hyperfiddle.electric-dom2 :as dom] - [hyperfiddle.electric-ui4 :as ui] - [hyperfiddle.popover :refer [Popover]] - #?(:clj [contrib.test.datomic-peer-mbrainz :as test]))) - -(def label-form-spec [:db/id - :label/gid - :label/name - :label/sortName - {:label/type [:db/ident]} - {:label/country [:db/ident]} - :label/startYear]) - -(comment (d/pull test/db ['*] test/cobblestone)) - -#?(:clj (defn type-options [db & [needle]] - (->> (d/q '[:find (pull ?e [:db/ident]) :in $ ?needle :where - [?e :db/ident ?ident] - [(namespace ?ident) ?x-ns] [(= ?x-ns "label.type")] - [(name ?ident) ?x-label] - [(contrib.str/includes-str? ?x-label ?needle)]] - db (or needle "")) - (map first)))) - -(comment - (type-options test/db "") - (type-options test/db "prod") - (type-options test/db "bootleg") - (type-options test/db nil)) - - -(e/defn Form [e] - (let [record (d/pull hf/db label-form-spec e)] - (e/client - (dom/dl - - (dom/dt (dom/text "id")) - (dom/dd (ui/input (:db/id record) nil (dom/props {::dom/disabled true}))) - - (dom/dt "gid") - (dom/dd (ui/uuid (:label/gid record) nil (dom/props {::dom/disabled true}))) - - (dom/dt (dom/text "name")) - (dom/dd (ui/input (:label/name record) - (e/fn [v] - (println 'input! v) - (e/server #_(when true) (hf/Transact!. [[:db/add e :label/name v]]))) - (dom/props {:id "name"}))) - - (dom/dt (dom/text "name2")) - (dom/dd (ui/input (:label/name record) - (e/fn [v] - (println 'input2! v) - (e/server #_(when true) (hf/Transact!. [[:db/add e :label/name v]]))) - (dom/props {:id "name2"}))) - - (dom/dt (dom/text "sortName")) - (dom/dd (ui/input (:label/sortName record) - (e/fn [v] (e/server (hf/Transact!. [[:db/add e :label/sortName v]]))))) - - - (dom/dt (dom/text "type")) - (dom/dd (e/server - (ui/typeahead - (:label/type record) - (e/fn V! [option] (hf/Transact!. [[:db/add e :label/type (:db/ident option)]])) - (e/fn Options [search] (type-options hf/db search)) - (e/fn OptionLabel [option] (-> option :db/ident name))))) - - ; country - - (dom/dt (dom/text "startYear")) - (dom/dd (ui/long (:label/startYear record) - (e/fn [v] (e/server (hf/Transact!. [[:db/add e :label/startYear v]]))))) - ) - - (dom/pre (dom/text (pprint-str record)))))) - -(e/defn Page [] - #_(e/client (dom/div (if hf/loading "loading" "idle") " " (str (hf/Load-timer.)) "ms")) - (Form. test/cobblestone) - #_(Form. test/cobblestone) - (e/client (Popover. "open" (e/fn [] (e/server (Form. test/cobblestone)))))) - -(e/defn CrudForm [] - (e/client (dom/h1 (dom/text (str `CrudForm)))) - (e/server - (let [conn @(requiring-resolve 'contrib.test.datomic-peer-mbrainz/conn) - secure-db (d/db conn)] ; todo datomic-tx-listener - (binding [hf/schema (new (dx/schema> secure-db)) - hf/into-tx' hf/into-tx - hf/with (fn [db tx] ; inject datomic - (try (:db-after (d/with db {:tx-data tx})) - (catch Exception e - (println "...failure, e: " e) - db))) - hf/db secure-db] - (hf/branch - (Page.) - (e/client - (dom/hr) - (dom/element "style" (str "." (css-slugify `staged) " { display: block; width: 100%; height: 10em; }")) - (ui/edn (e/server hf/stage) nil (dom/props {::dom/disabled true ::dom/class (css-slugify `staged)})))))))) diff --git a/src-triage/electric_demo/wip/demo_todos_advanced.cljc b/src-triage/electric_demo/wip/demo_todos_advanced.cljc deleted file mode 100644 index 513f4d1..0000000 --- a/src-triage/electric_demo/wip/demo_todos_advanced.cljc +++ /dev/null @@ -1,146 +0,0 @@ -(ns electric-demo.wip.demo-todos-advanced - (:import [hyperfiddle.electric Pending]) - (:require #?(:clj [datomic.api :as d]) ; database on server - [hyperfiddle.electric :as e] - [hyperfiddle.electric-dom2 :as dom] - [missionary.core :as m] - [hyperfiddle.electric-ui4 :as ui] - [hyperfiddle.electric-ui5 :as ui5] - [contrib.data] - [contrib.debug :as dbg])) - -;; showcases how to render an item optimistically and without e/for-event -;; missing: -;; - ordering -;; - idempotent entity creation -;; - integration of a local and remote seq in e/for-by - -#?(:clj - (def schema - [{:db/ident :task/status, :db/valueType :db.type/keyword, :db/cardinality :db.cardinality/one} - {:db/ident :task/description, :db/valueType :db.type/string, :db/cardinality :db.cardinality/one} - {:db/ident :hf/stable-id, :db/valueType :db.type/uuid, :db/cardinality :db.cardinality/one, :db/unique :db.unique/identity}])) - -#?(:clj (defn init-conn [] - (let [uri "datomic:mem://db"] - (d/delete-database uri) - (d/create-database uri) - (let [conn (d/connect uri)] - (d/transact conn schema) - conn)))) - -(defonce !conn #?(:clj (init-conn) :cljs nil)) ; database on server -#?(:clj (comment (alter-var-root #'!conn (fn [_] (init-conn))))) -(e/def db) - -; injected database ref; Electric defs are always dynamic -(defonce !db #?(:clj (atom nil) :cljs nil)) -;; singleton database queue polling -;; in the future this can be done with `m/signal` -(defonce !taker #?(:clj (future - (reset! !db (d/db !conn)) - (let [q (d/tx-report-queue !conn)] - (loop [] - (reset! !db (:db-after (.take ^java.util.concurrent.LinkedBlockingQueue q))) - (recur)))) - :cljs nil)) - -;; user configurable latency and tx fail rate -#?(:clj (def !latency (atom 200))) -(e/def latency (e/server (e/watch !latency))) - -#?(:clj (def !fail-rate (atom 1))) -(e/def fail-rate (e/server (e/watch !fail-rate))) - -;; tx with configured latency and fail rate -#?(:clj (defn tx! [tx] - (m/sp - (m/? (m/sleep @!latency)) - (if (< (rand-int 10) @!fail-rate) - (throw (ex-info "tx failed" {:tx tx})) - @(d/transact !conn (dbg/dbg :tx tx)))))) - -(e/def Tx!) - -(e/defn Latency [min max] - (dom/span (dom/style {:display "inline-flex", :flex-direction "column"}) - (dom/span (dom/text "Latency: " latency "ms")) - (ui/range latency (e/fn [v] (e/server (reset! !latency v))) - (dom/props {:min min, :max max, :style {:width "200px"}})))) - -(e/defn FailRate [min max] - (dom/span (dom/style {:display "inline-flex", :flex-direction "column"}) - (dom/span (dom/text "Fail Rate: " fail-rate " out of " max)) - (ui/range fail-rate (e/fn [v] (e/server (reset! !fail-rate v))) - (dom/props {:min min, :max max, :style {:width "200px"}})))) - -#?(:clj (defn todo-count [db] (count (d/q '[:find [?e ...] :where [?e :task/status :active]] db)))) - -#?(:clj (defn todo-records [db] - (d/q '[:find [(pull ?e [:db/id :task/description :task/status :hf/stable-id]) ...] - :where [?e :task/status]] db))) - -(def tempid? (some-fn nil? string?)) - -(e/defn ReadEntity [id] - (try - (e/server [::e/init (into {} (d/touch (d/entity db id)))]) - (catch Pending _ [::e/pending nil]) - (catch :default e [::e/failed e]))) - -(e/defn CreateEntity [id record] - (try ; create is never ::e/init - (e/server - (when-not (d/entity db id) - (case (new Tx! [record]) ; returns tx-report which has :ids->tempids - [::e/ok (into {} (d/touch (d/entity db id)))]))) - (catch Pending _ [::e/pending record]) ; optimistic - (catch :default e [::e/failed e]))) - -(e/defn EnsureEntity [id record] - (if-not (tempid? id) - (ReadEntity. id) - (CreateEntity. id record))) ; todo must be idempotent - -(e/defn TodoItem [record] - (e/client - (dom/div (dom/style {:display "flex", :align-items "center"}) - (ui5/entity record EnsureEntity - (ui5/checkbox (= :done (:task/status record)) - (e/fn [checked?] (e/server (new Tx! [[:db/add (:db/id record) :task/status (if checked? :done :active)]])))) - (ui5/input (:task/description record) - (e/fn [v] (e/server (new Tx! [[:db/add (:db/id record) :task/description v]])))))))) - -(e/defn AdvancedTodoList [] - (e/server - (binding [db (e/watch !db), Tx! (e/fn [tx] (new (e/task->cp (tx! tx))) nil)] - ;; (d/transact !conn schema) - (e/client - (dom/h1 (dom/text "advanced todo list with optimistic render and fail/retry")) - (dom/p (dom/text "it's multiplayer, try two tabs")) - (Latency. 0 2000) - (FailRate. 0 10) - (dom/div (dom/props {:class "todo-list"}) - ;(dom/div {:class "todo-items"}) - (let [optimistic-records - (dom/input (dom/props {:placeholder "Buy milk"}) ; todo move into TodoItem - (->> (m/observe (fn [!] (e/dom-listener dom/node "keydown" #(some-> (ui/?read-line! dom/node %) !) {}))) - (m/eduction (map (fn [input-val] - (let [id (random-uuid)] - {:hf/stable-id id - :task/description input-val - :task/status :active})))) - (m/reductions conj []) - new))] - (e/client #_e/server ; fixme - ;; we have a local and remote list of records - ;; we'd need to diff them on their respective peers and integrate them on the client - ;; currently we don't have this functionality so we hack it - ;; by sending the whole server collection to the client - (e/for-by :hf/stable-id [record (vals (reduce (fn [ac nx] (assoc ac (:hf/stable-id nx) nx)) - (contrib.data/index-by :hf/stable-id optimistic-records) - (e/server (todo-records db))))] - (TodoItem. record)))) - (dom/p (dom/props {:class "counter"}) - (dom/span (dom/props {:class "count"}) (dom/text (e/server (todo-count db)))) - (dom/text " items left"))))))) diff --git a/src-triage/electric_demo/wip/demo_todos_advanced_old.cljc b/src-triage/electric_demo/wip/demo_todos_advanced_old.cljc deleted file mode 100644 index 62c3ca4..0000000 --- a/src-triage/electric_demo/wip/demo_todos_advanced_old.cljc +++ /dev/null @@ -1,118 +0,0 @@ -(ns electric-demo.wip.demo-todos-advanced-old - (:import [hyperfiddle.electric Pending] - [missionary Cancelled]) - (:require #?(:clj [datascript.core :as d]) ; database on server - [hyperfiddle.electric :as e] - [hyperfiddle.electric-dom2 :as dom] - [missionary.core :as m] - [hyperfiddle.electric-ui4 :as ui] - [contrib.debug :as dbg])) - -(defonce !conn #?(:clj (d/create-conn {}) :cljs nil)) ; database on server -(comment (alter-var-root #'!conn (fn [_] (d/create-conn {})))) -(e/def db) ; injected database ref; Electric defs are always dynamic - -;; auto-incrementing task id, to define ordering -;; an optimistically rendered task won't jump on the screen -(defonce !order-id #?(:clj (atom 0) :cljs nil)) - -;; user configurable latency and tx fail rate -#?(:clj (def !latency (atom 200))) -(e/def latency (e/server (e/watch !latency))) - -#?(:clj (def !fail-rate (atom 1))) -(e/def fail-rate (e/server (e/watch !fail-rate))) - -;; tx with configured latency and fail rate -#?(:clj (defn tx! [tx] - (m/sp - (m/? (m/sleep @!latency)) - (if (< (rand-int 10) @!fail-rate) - (throw (ex-info "tx failed" {:tx tx})) - (d/transact! !conn tx))))) - -(e/def Tx!) - -(e/defn Latency [min max] - (dom/span (dom/style {:display "inline-flex", :flex-direction "column"}) - (dom/span (dom/text "Latency: " latency "ms")) - (ui/range latency (e/fn [v] (e/server (reset! !latency v))) - (dom/props {:min min, :max max, :style {:width "200px"}})))) - -(e/defn FailRate [min max] - (dom/span (dom/style {:display "inline-flex", :flex-direction "column"}) - (dom/span (dom/text "Fail Rate: " fail-rate " out of " max)) - (ui/range fail-rate (e/fn [v] (e/server (reset! !fail-rate v))) - (dom/props {:min min, :max max, :style {:width "200px"}})))) - -(defn ->task [desc] {:task/description desc, :task/status :active, :task/order (swap! !order-id inc)}) - -#?(:clj (defn todo-count [db] (count (d/q '[:find [?e ...] :where [?e :task/status :active]] db)))) - -#?(:clj (defn todo-records [db] - (->> (d/q '[:find [(pull ?e [:db/id :task/description :task/order]) ...] :where [?e :task/status]] db) - (sort-by :task/order #(compare %2 %1))))) - -(e/defn TodoItem [id] - (e/server - (let [e (d/entity db id) - status (:task/status e) - server-checked? (= :done status)] - (e/client - (dom/div - ;; TODO a failed tick won't revert, is that expected/good? - (ui/checkbox server-checked? - (e/fn [checked?] - (e/server (new Tx! [[:db/add id :task/status (if checked? :done :active)]]))) - (dom/props {:id id})) - (dom/label (dom/props {:for id}) (dom/text (e/server (:task/description e))))))))) - -(e/defn AdvancedTodoList [] - (e/server - (binding [db (e/watch !conn), Tx! (e/fn [tx] (new (e/task->cp (tx! tx))) nil)] - (e/client - (dom/h1 (dom/text "advanced todo list with optimistic render and fail/retry")) - (dom/p (dom/text "it's multiplayer, try two tabs")) - (Latency. 0 2000) - (FailRate. 0 10) - (dom/div (dom/props {:class "todo-list"}) - ;; we have to tuck away the input node because we'll be mounting nodes under a