├── .clj-kondo
└── config.edn
├── .cljfmt.edn
├── .dockerignore
├── .github
└── workflows
│ └── deploy.yml
├── .gitignore
├── .gitmodules
├── .idea
└── ClojureProjectResolveSettings.xml
├── .m2
└── settings.xml
├── Dockerfile
├── README.md
├── deps.edn
├── fly.toml
├── package.json
├── resources
└── public
│ ├── favicon.ico
│ ├── index.html
│ ├── todo-list.css
│ └── user
│ ├── demo_stage_ui4.css
│ ├── examples.css
│ ├── github-markdown.css
│ └── gridsheet-optional.css
├── shadow-cljs.edn
├── src-build
├── build.clj
└── build.md
├── src
├── contrib
│ └── datafy_fs.clj
├── dev.cljc
├── electric_server_java11_jetty10.clj
├── electric_server_java8_jetty9.clj
├── logback.xml
├── prod.clj
├── test.clj
├── test
│ ├── mbrainz.clj
│ ├── person_model.clj
│ ├── seattle.clj
│ ├── sku_model.clj
│ └── social_news.clj
├── toxiproxy.sh
├── user.clj
├── user.cljs
├── user.md
├── user
│ ├── demo_10k_dom.cljc
│ ├── demo_chat.cljc
│ ├── demo_chat.md
│ ├── demo_chat_extended.cljc
│ ├── demo_chat_extended.md
│ ├── demo_color.cljc
│ ├── demo_explorer.cljc
│ ├── demo_explorer.md
│ ├── demo_index.cljc
│ ├── demo_reagent_interop.cljc
│ ├── demo_svg.cljc
│ ├── demo_svg.md
│ ├── demo_system_properties.cljc
│ ├── demo_system_properties.md
│ ├── demo_tic_tac_toe.cljc
│ ├── demo_todomvc.cljc
│ ├── demo_todomvc.md
│ ├── demo_todomvc_composed.cljc
│ ├── demo_todomvc_composed.md
│ ├── demo_todos_simple.cljc
│ ├── demo_todos_simple.md
│ ├── demo_toggle.cljc
│ ├── demo_toggle.md
│ ├── demo_two_clocks.cljc
│ ├── demo_two_clocks.md
│ ├── demo_virtual_scroll.cljc
│ ├── demo_webview.cljc
│ ├── demo_webview.md
│ ├── example_datascript_db.clj
│ ├── tutorial_7guis_1_counter.cljc
│ ├── tutorial_7guis_2_temperature.cljc
│ ├── tutorial_7guis_4_timer.cljc
│ ├── tutorial_7guis_5_crud.cljc
│ ├── tutorial_backpressure.cljc
│ ├── tutorial_backpressure.md
│ ├── tutorial_lifecycle.cljc
│ └── tutorial_lifecycle.md
├── user_main.cljc
└── wip
│ ├── demo_branched_route.cljc
│ ├── demo_custom_types.cljc
│ ├── demo_explorer2.cljc
│ ├── demo_stage_ui4.cljc
│ ├── js_interop.cljc
│ ├── orders_datascript.cljc
│ ├── orders_datomic.clj
│ ├── tag_picker.cljc
│ ├── teeshirt_orders.cljc
│ ├── teeshirt_orders.md
│ └── tracing.cljc
└── yarn.lock
/.clj-kondo/config.edn:
--------------------------------------------------------------------------------
1 | {:lint-as {hyperfiddle.electric/def clojure.core/def
2 | hyperfiddle.electric/defn clojure.core/defn
3 | hyperfiddle.electric/for clojure.core/for
4 | hyperfiddle.electric/fn clojure.core/fn}
5 | :linters {:redundant-expression {:level :off}}}
6 |
--------------------------------------------------------------------------------
/.cljfmt.edn:
--------------------------------------------------------------------------------
1 | {:indents ^:replace {#"^." [[:inner 0]]}
2 | :test-code [(sui/ui-grid {:columns 2}
3 | (sui/ui-grid-row {}
4 | (sui/ui-grid-column {:width 12}
5 | ...)))
6 | (let [foo bar]
7 | (str "foo"
8 | "bar"))]}
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | # flyctl launch added from .gitignore
2 | **/node_modules
3 | **/resources/public/js
4 | **/.cpcache
5 | **/.shadow-cljs
6 | **/.nrepl-port
7 | **/.clj-kondo/.cache
8 | **/yarn-error.log
9 | **/report.html
10 | **/.idea/*
11 | !**/.idea/ClojureProjectResolveSettings.xml
12 | **/*.iml
13 | **/.lsp
14 | **/.clj-kondo
15 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | name: hyperfiddle/electric-examples-app
2 | on:
3 | push:
4 | branches:
5 | - "main"
6 | pull_request:
7 | jobs:
8 | deploy:
9 | name: Deploy to Fly.io
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v3
13 | with:
14 | fetch-depth: 0
15 | - uses: superfly/flyctl-actions/setup-flyctl@master
16 | - run: NO_COLOR=1 flyctl deploy --build-arg VERSION=$(git describe --tags --long --always --dirty) --build-arg DATOMIC_DEV_LOCAL_USER=$DATOMIC_DEV_LOCAL_USER --build-arg DATOMIC_DEV_LOCAL_PASSWORD=$DATOMIC_DEV_LOCAL_PASSWORD --build-arg hfql_ssh_prv_key="$HFQL_SSH_PRV_KEY" --remote-only
17 | env:
18 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
19 | DATOMIC_DEV_LOCAL_USER: ${{ secrets.DATOMIC_DEV_LOCAL_USER }}
20 | DATOMIC_DEV_LOCAL_PASSWORD: ${{ secrets.DATOMIC_DEV_LOCAL_PASSWORD }}
21 | HFQL_SSH_PRV_KEY: ${{ secrets.HFQL_SSH_PRV_KEY }}
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | /!.idea/ClojureProjectResolveSettings.xml
3 | /.calva
4 | /.clj-kondo
5 | /.clj-kondo/.cache
6 | /.cpcache
7 | /.idea/*
8 | /.lsp/.cache
9 | /.nrepl-port
10 | /.shadow-cljs
11 | /.vscode
12 | /node_modules
13 | /report.html
14 | /resources/public/js
15 | /target
16 | /yarn-error.log
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "electric"]
2 | path = vendors/electric
3 | url = git@github.com:hyperfiddle/electric.git
4 |
--------------------------------------------------------------------------------
/.idea/ClojureProjectResolveSettings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | IDE
5 |
6 |
--------------------------------------------------------------------------------
/.m2/settings.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 | cognitect-dev-tools
9 | ${env.DATOMIC_DEV_LOCAL_USER}
10 | ${env.DATOMIC_DEV_LOCAL_PASSWORD}
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:14.7-stretch AS node-deps
2 | WORKDIR /app
3 | COPY package.json package.json
4 | RUN npm install
5 |
6 | FROM clojure:openjdk-11-tools-deps AS build
7 | WORKDIR /app
8 | COPY --from=node-deps /app/node_modules /app/node_modules
9 |
10 | ARG DATOMIC_DEV_LOCAL_USER
11 | ARG DATOMIC_DEV_LOCAL_PASSWORD
12 | ENV DATOMIC_DEV_LOCAL_USER=$DATOMIC_DEV_LOCAL_USER
13 | ENV DATOMIC_DEV_LOCAL_PASSWORD=$DATOMIC_DEV_LOCAL_PASSWORD
14 |
15 | # Authorize SSH Host
16 | RUN mkdir -p /root/.ssh && \
17 | chmod 0700 /root/.ssh && \
18 | ssh-keyscan github.com >> /root/.ssh/known_hosts
19 |
20 | # Create a wrapper for clojure binary
21 | ARG hfql_ssh_prv_key
22 | # Private keys MUST be written to a file this way ("$var") and end with a newline
23 | # If passed as a command arg directly, Github shell will strip newlines and corrupt it.
24 | RUN echo "$hfql_ssh_prv_key" > /root/.ssh/id_rsa
25 |
26 | RUN mkdir -p /usr/local/sbin/ && \
27 | echo -e '#!/bin/sh \n eval $(ssh-agent -s) && exec /usr/local/bin/clojure "$@"' >> /usr/local/sbin/clojure && \
28 | chmod +x /usr/local/sbin/clojure
29 |
30 | COPY .m2 /root/.m2
31 | COPY shadow-cljs.edn shadow-cljs.edn
32 | COPY deps.edn deps.edn
33 | COPY src src
34 | COPY src-build src-build
35 | COPY resources resources
36 | ARG REBUILD=unknown
37 | ARG VERSION
38 | RUN clojure -T:build build-client :verbose true :version '"'$VERSION'"'
39 |
40 | ENV VERSION=$VERSION
41 | CMD clj -J-DELECTRIC_USER_VERSION=$VERSION -M -m prod
42 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Examples app — Electric Clojure (v2)
2 |
3 | Live app: https://electric2.hyperfiddle.net/
4 |
5 |
--------------------------------------------------------------------------------
/deps.edn:
--------------------------------------------------------------------------------
1 | {:paths ["src" "resources"]
2 | :deps {com.datomic/dev-local {:mvn/version "1.0.243"}
3 | com.google.guava/guava {:mvn/version "31.1-jre"} ; fix conflict - datomic cloud & shadow
4 | com.hyperfiddle/electric {:mvn/version "v2-alpha-536-g0c582f78"}
5 | com.hyperfiddle/rcf {:mvn/version "20220926-202227"}
6 | datascript/datascript {:mvn/version "1.5.2"}
7 | com.datomic/peer {:mvn/version "1.0.6735"}
8 | info.sunng/ring-jetty9-adapter
9 | {:mvn/version "0.14.3" ; (Jetty 9) is Java 8 compatible;
10 | ;:mvn/version "0.17.7" ; (Jetty 10) is NOT Java 8 compatible
11 | :exclusions [org.slf4j/slf4j-api info.sunng/ring-jetty9-adapter-http3]} ; no need
12 | org.clojure/clojure {:mvn/version "1.12.0-alpha4"}
13 | org.clojure/clojurescript {:mvn/version "1.11.60"}
14 | org.clojure/tools.logging {:mvn/version "1.2.4"}
15 | ch.qos.logback/logback-classic {:mvn/version "1.2.11"}
16 | ring-basic-authentication/ring-basic-authentication {:mvn/version "1.1.1"}
17 | reagent/reagent {:mvn/version "1.1.1"}
18 | markdown-clj/markdown-clj {:mvn/version "1.11.4"}
19 | nextjournal/clojure-mode {:git/url "https://github.com/nextjournal/clojure-mode"
20 | :sha "ac038ebf6e5da09dd2b8a31609e9ff4a65e36852"}}
21 | :aliases {:dev
22 | {:extra-deps
23 | {binaryage/devtools {:mvn/version "1.0.6"}
24 | com.clojure-goes-fast/clj-async-profiler {:mvn/version "1.1.1"}
25 | thheller/shadow-cljs {:mvn/version "2.22.10"}}
26 | :override-deps
27 | {com.hyperfiddle/electric {:local/root "vendors/electric"}}
28 | :jvm-opts
29 | ["-Xss2m" ; https://github.com/hyperfiddle/photon/issues/11
30 | "-XX:-OmitStackTraceInFastThrow" ;; RCF
31 | "-Djdk.attach.allowAttachSelf"
32 | ]
33 | :exec-fn user/main
34 | :exec-args {}}
35 | :build
36 | {:extra-paths ["src-build"]
37 | :ns-default build
38 | :extra-deps {io.github.clojure/tools.build {:git/tag "v0.8.2" :git/sha "ba1a2bf"}
39 | io.github.seancorfield/build-clj {:git/tag "v0.8.0" :git/sha "9bd8b8a"}}
40 | :jvm-opts ["-Xss2m"]}
41 | :shadow-cljs {:extra-deps {thheller/shadow-cljs {:mvn/version "2.22.10"}}
42 | :main-opts ["-m" "shadow.cljs.devtools.cli"]
43 | :jvm-opts ["-Xss2m"]}
44 | :hfql {:extra-deps {com.hyperfiddle/hfql {:git/url "git@github.com:hyperfiddle/hfql.git"
45 | :git/sha "39458cc87e3adeb7ed78293198c35d0fdca5d5a4"}}}}
46 | :mvn/repos {"cognitect-dev-tools" {:url "https://dev-tools.cognitect.com/maven/releases/"}}}
47 |
--------------------------------------------------------------------------------
/fly.toml:
--------------------------------------------------------------------------------
1 | # fly.toml file generated for electric-examples-app on 2023-03-31T18:34:16+02:00
2 |
3 | app = "electric-examples-app"
4 | kill_signal = "SIGINT"
5 | kill_timeout = 5
6 | primary_region = "ewr"
7 | processes = []
8 |
9 | [env]
10 |
11 | [experimental]
12 | allowed_public_ports = []
13 | auto_rollback = true
14 |
15 | [[services]]
16 | http_checks = []
17 | internal_port = 8080
18 | processes = ["app"]
19 | protocol = "tcp"
20 | script_checks = []
21 | [services.concurrency]
22 | hard_limit = 200
23 | soft_limit = 150
24 | type = "connections"
25 |
26 | [[services.ports]]
27 | force_https = true
28 | handlers = ["http"]
29 | port = 80
30 |
31 | [[services.ports]]
32 | handlers = ["tls", "http"]
33 | port = 443
34 |
35 | [[services.tcp_checks]]
36 | grace_period = "1s"
37 | interval = "15s"
38 | restart_limit = 0
39 | timeout = "2s"
40 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "@codemirror/commands": "^6.2.2",
4 | "@codemirror/language": "^6.6.0",
5 | "@codemirror/view": "^6.9.3",
6 | "@lezer/markdown": "^1.0.2",
7 | "@nextjournal/lezer-clojure": "^1.0.0",
8 | "lezer-clojure": "0.1.10",
9 | "recharts": "^2.4.1",
10 | "react": "18.2.0",
11 | "react-dom": "18.2.0",
12 | "prop-types": "15.8.1"
13 | },
14 | "devDependencies": {
15 | "karma": "6.4.0",
16 | "karma-chrome-launcher": "3.1.1",
17 | "karma-cljs-test": "0.1.0",
18 | "puppeteer": "15.2.0",
19 | "shadow-cljs": "^2.20.1"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/resources/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hyperfiddle/electric-v2-tutorial/fb5df89804ecc2d02916573897bb7ecca23940fe/resources/public/favicon.ico
--------------------------------------------------------------------------------
/resources/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Hyperfiddle
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/resources/public/todo-list.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@100;200;300;400;500;600;700&display=swap');
2 |
3 | body{
4 | font-family: 'IBM Plex Sans', sans-serif;
5 | font-weight: 400;
6 | }
7 |
8 | input[type="text"]{
9 | padding: 0.5rem;
10 | margin-bottom: 1rem;
11 | min-width: 30rem;
12 | border-radius: 0.25rem;
13 | border: 1px gray solid;
14 | }
15 |
16 | input[type="checkbox"]{
17 | width: 1rem;
18 | height: 1rem;
19 | }
20 |
21 | input[type="checkbox"]:checked + label{
22 | text-decoration: line-through;
23 | color: gray;
24 | }
25 |
26 | label{
27 | padding-left: 0.5rem;
28 | }
29 |
30 | label, input[type="checkbox"]{
31 | cursor:pointer;
32 | transition: 0.4s ease all;
33 | }
34 |
35 | label::first-letter{
36 | text-transform: capitalize;
37 | }
38 |
39 | .todo-list{
40 | display:grid;
41 | grid-template-rows: auto fit-content auto;
42 | max-width: 30rem;
43 | }
44 |
45 | .todo-items{
46 | display:grid;
47 | grid-template-columns: auto 1fr;
48 | grid-gap: 0.5rem 0;
49 | padding: 0 0.25rem;
50 | }
51 |
52 | .counter{
53 | justify-self: center;
54 | }
55 |
56 | .count{
57 | font-size: 1.25rem;
58 | }
--------------------------------------------------------------------------------
/resources/public/user/demo_stage_ui4.css:
--------------------------------------------------------------------------------
1 | .wip-demo-stage-ui4-staged {
2 | display: block;
3 | width: 100%;
4 | height: 10em;
5 | }
--------------------------------------------------------------------------------
/resources/public/user/examples.css:
--------------------------------------------------------------------------------
1 | body.hyperfiddle{
2 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
3 | -webkit-text-size-adjust: none;
4 | text-size-adjust: none;
5 |
6 | display: grid;
7 | grid-template-areas: "title title"
8 | "nav nav"
9 | "lead lead"
10 | "code result"
11 | "readme readme"
12 | "footer-nav footer-nav";
13 | grid-template-rows: auto 1.5rem auto auto min-content;
14 | grid-template-columns: min-content auto;
15 | gap: 1rem;
16 | margin: 0;
17 | padding: 1rem;
18 | overflow-y: auto;
19 | overflow-x: hidden;
20 |
21 | box-sizing: content-box;
22 | position: relative;
23 | background-color:white;
24 | }
25 |
26 | .hyperfiddle.user-examples-demo {
27 | grid-template-areas: "title"
28 | "nav"
29 | "lead"
30 | "result"
31 | "readme"
32 | "footer-nav";
33 | grid-template-columns: auto;
34 |
35 | }
36 |
37 | .hyperfiddle.user-examples fieldset{
38 | background-color: white;
39 | overflow: auto;
40 | min-inline-size: auto;
41 | }
42 | .hyperfiddle.user-examples fieldset legend{
43 | margin: 0 1rem;
44 | }
45 |
46 |
47 | .user-examples > h1{
48 | grid-area: title;
49 | white-space: nowrap;
50 | }
51 |
52 |
53 | .user-examples-nav{
54 | grid-area: nav;
55 | }
56 |
57 | .user-examples-footer-nav{
58 | grid-area: footer-nav;
59 | padding-bottom: 10vh;
60 | }
61 |
62 | .user-examples-nav,
63 | .user-examples-footer-nav{
64 | display: grid;
65 | grid-auto-flow: column;
66 | width: auto;
67 | place-items: center;
68 | align-self:center;
69 | justify-self: start;
70 | }
71 |
72 | .user-examples-nav *,
73 | .user-examples-footer-nav *{
74 | white-space: nowrap;
75 | overflow: hidden;
76 | text-overflow: ellipsis;
77 | max-width: 100%;
78 | }
79 |
80 | .user-examples-select{
81 | margin: 0 1rem;
82 | display:flex;
83 | align-items: center;
84 | position: relative;
85 | }
86 |
87 | .user-examples-select > select{
88 | appearance: none;
89 | flex:1;
90 | border: 1px gray solid;
91 | border-radius: 3px;
92 | padding: 0.25rem 3rem 0.25rem 0.5rem;
93 | }
94 |
95 | .user-examples-select > svg{
96 | width: 1rem;
97 | position: absolute;
98 | right: 0.5rem;
99 | pointer-events:none;
100 | }
101 |
102 | .user-examples-lead {
103 | grid-area: lead;
104 | }
105 |
106 | .user-examples-target{
107 | grid-area:result;
108 | padding: 1rem;
109 | max-height: 41rem;
110 | }
111 |
112 | .user-examples-code{
113 | grid-area:code;
114 | padding: 0;
115 | height: fit-content;
116 | max-width: 100%;
117 | max-height: 40rem;
118 | padding-right: 1rem;
119 | }
120 |
121 | .user-examples-readme{
122 | grid-area:readme;
123 | padding-bottom: 10vh;
124 | line-height: 24px;
125 | max-width: 80ch;
126 | }
127 |
128 |
129 | @media (max-width: 980px) and (hover: none) and (pointer: coarse) {
130 | .user-examples-select > select{
131 | padding: 0.5rem 3rem 0.5rem 1rem;
132 | }
133 | .user-examples-select optgroup, .user-examples-select option{
134 | font-size: 1rem;
135 | }
136 | .user-examples-code{
137 | overflow: scroll;
138 | margin:0;
139 | }
140 | .user-examples-target.SystemProperties input{
141 | font-size: 1em;
142 | }
143 |
144 | .user-examples-target.SystemProperties td{
145 | white-space: nowrap;
146 | }
147 |
148 | }
149 |
150 | @media (max-width: 980px){
151 | body.hyperfiddle{
152 | display: flex;
153 | flex-direction: column;
154 | grid-template-areas: "title" "nav" "lead" "result" "code" "readme" "footer-nav";
155 | grid-template-rows: auto auto auto min-content auto minmax(20rem, 1fr)
156 | grid-template-columns: 100%;
157 | }
158 |
159 | .user-examples-code{
160 | max-height: initial;
161 | }
162 |
163 | .user-examples-target{
164 | padding: 1rem;
165 | overflow:auto;
166 | max-height: 50vh;
167 | }
168 |
169 | .user-examples-nav,
170 | .user-examples-footer-nav{
171 | grid-template-areas: "select select" "prev next";
172 | gap: 1rem;
173 | }
174 | .user-examples-nav-start{
175 | grid-template-areas: "select next";
176 | }
177 |
178 | .user-examples-nav-end{
179 | grid-template-areas: "prev select";
180 | }
181 |
182 | .user-examples-nav .user-examples-nav-prev {
183 | grid-area: prev;
184 | }
185 | .user-examples-nav .user-examples-nav-next {
186 | grid-area: next;
187 | }
188 |
189 | .user-examples-select{
190 | grid-area: select;
191 | }
192 |
193 | .user-examples-select > select{
194 | font-size: 1em;
195 | padding: 0.25em 3em 0.25rem 0.5rem;
196 | }
197 | .user-examples-select > svg{
198 | width: 1em;
199 | right: 1em;
200 | }
201 | }
202 |
203 | @media (orientation: landscape) and (hover: none) and (pointer: coarse) {
204 | body.hyperfiddle{
205 | font-size: 16px;
206 | grid-template-areas: "title" "nav" "lead" "result" "code" "readme" "footer-nav";
207 | grid-template-rows: auto auto auto min-content min-content auto;
208 | grid-template-columns: 100%;
209 | line-height: initial;
210 | }
211 |
212 | body.hyperfiddle h1 {
213 | margin:0;
214 | }
215 |
216 | .user-examples-select{
217 | font-size: 1rem;
218 | }
219 |
220 | .user-examples-code{
221 | font-size: 1rem;
222 | }
223 | .user-examples-target{
224 | max-height: 13rem;
225 | }
226 |
227 | }
228 |
229 | .user-examples-target.SystemProperties input{
230 | margin: 0.25em 0 1rem 0;
231 | padding: 0.25em;
232 | }
233 |
234 |
235 | .user-examples-target.SystemProperties table{
236 | max-width: 100%;
237 | width: 100%;
238 | }
239 |
240 | .user-examples-target.SystemProperties td{
241 | white-space: nowrap;
242 | text-overflow: ellipsis;
243 | overflow: hidden;
244 | }
245 |
246 | @media (min-width: 981px) and (max-width: 1200px) {}
247 | @media (min-width: 1201px) {}
248 |
249 |
250 | .user-examples-target.Webview-HFQL{
251 | padding:0;
252 | }
253 |
254 | .user-examples-target.Webview-HFQL .wip\.teeshirt-orders\/orders.needle,
255 | .user-examples-target.Webview-HFQL .wip\.orders-datascript\/orders.needle{
256 | width: 9rem;
257 | padding: 0 0.5em;
258 | }
259 |
260 | .user-examples-target.Webview-HFQL label:has(+ .wip\.orders-datascript\/orders.needle) {
261 | position:relative;
262 | }
263 | .user-examples-target.Webview-HFQL label:has(+ .wip\.orders-datascript\/orders.needle):after {
264 | content: "🔎";
265 | position: absolute;
266 | right: calc(-100% + 0.25em);
267 | pointer-events: none;
268 | filter: grayscale(100%) opacity(75%);
269 | }
270 |
271 | .user-examples-target.Webview-HFQL .hyperfiddle-gridsheet-wrapper{
272 | min-width: 100%;
273 | width: fit-content;
274 | height: 100%;
275 | margin: 0;
276 | box-sizing: border-box;
277 | border: 0;
278 | }
279 |
280 | .user-examples-target.Webview-HFQL .hyperfiddle-gridsheet{
281 | grid-template-columns: 9rem min-content repeat(3, max-content);
282 | white-space: nowrap;
283 | word-break : keep-all;
284 | line-height: initial;
285 | min-width: 100%;
286 | }
287 |
288 |
289 | .user-examples-target.DirectoryExplorer{
290 | font-size: initial;
291 | line-height: initial;
292 | max-height: 90vh;
293 | }
294 |
295 | .user-examples-target.Chat input,
296 | .user-examples-target.ChatExtended input,
297 | .user-examples-target.Webview input,
298 | .user-examples-target.TodoList input
299 | {
300 | font-size: 1em;
301 | padding: 0.25em 0.5em;
302 | }
303 |
304 | .user-examples-target.Webview input{
305 | margin-bottom: 1em;
306 | }
307 |
308 | .user-examples-target.Lifecycle{
309 | height: 9rem;
310 | }
311 |
312 |
313 | .user-examples-target.TodoList .todo-items{
314 | list-style-type: none;
315 | margin: 0;
316 | padding: 0;
317 | }
318 |
319 | .user-examples-target.TodoList .todo-items > li {
320 | margin: 0.25em 0;
321 | }
322 | .user-examples-target.TodoList .todo-items > li > input[type=checkbox]{
323 | margin-right: 0.5em;
324 | }
325 |
326 |
327 | @media (max-width: 980px){
328 | .user-examples-target.TodoList .todo-items {
329 | padding-top: 0.5em;
330 | }
331 | .user-examples-target.TodoList .todo-items > li {
332 | display: flex;
333 | align-items: center;
334 | margin: 0.5em 0;
335 | }
336 | .user-examples-target.TodoList .todo-items > li > input[type=checkbox]{
337 | width: 1.5em;
338 | height: 1.5em;
339 | }
340 | }
341 |
342 | .user-examples-target.ReagentInterop{
343 | max-height: initial;
344 | height: min-content;
345 | padding: 0;
346 | margin: 0;
347 | }
348 |
349 | .user-examples-target.CustomTypes{
350 | white-space: nowrap;
351 | }
352 |
353 |
354 | .user-examples-target.TodoMVC{
355 | padding: 0.5em;
356 | }
357 | .user-examples-target.TodoMVC .todomvc,
358 | .user-examples-target.TodoMVC-composed .todomvc{
359 | max-width: 40em;
360 | }
361 |
362 | .user-examples-target.TodoMVC-composed .todomvc > div{
363 | min-width: 40em;
364 | }
365 |
366 | @media (max-width: 980px){
367 | .user-examples-target.TodoMVC .todomvc,
368 | .user-examples-target.TodoMVC-composed .todomvc{
369 | margin: auto;
370 | }
371 |
372 | .user-examples-target.TodoMVC-composed .todomvc > div{
373 | min-width: minmax(40em,100vw);
374 | }
375 |
376 | }
377 |
378 | .user-examples-target.TodoMVC-composed {
379 | min-height: 70vh;
380 | height: 100vh;
381 | }
382 |
383 | .user-examples-target.CrudForm .wip-demo-stage-ui4-staged{
384 | width: 100%;
385 | }
386 |
387 |
388 | .user-examples-target.QRCode {
389 | max-height: 250px;
390 | height: 250px;
391 | }
392 |
393 | .user-examples-target.QRCode input{
394 | margin-bottom: 1rem;
395 | }
396 |
--------------------------------------------------------------------------------
/resources/public/user/gridsheet-optional.css:
--------------------------------------------------------------------------------
1 | /* Cosmetic styles for the gridsheet demos; doesn't impact the scroll */
2 |
3 | .hyperfiddle .user-gridsheet-demo > [role=grid] > [role=columnheader] {
4 | white-space: nowrap;
5 | overflow-x: clip;
6 | text-overflow: ellipsis;
7 | }
8 |
9 | .hyperfiddle .user-gridsheet-demo > [role=grid] > [role=group] > [role=gridcell] {
10 | white-space: nowrap;
11 | overflow: hidden;
12 | text-overflow: ellipsis;
13 | }
14 |
15 | .hyperfiddle .user-gridsheet-demo > [role=grid] > [role=group]:nth-child(even) > div {
16 | background-color: #f2f2f2;
17 | }
18 |
19 | .hyperfiddle .user-gridsheet-demo > [role=grid] > [role=group]:hover > div {
20 | background-color: #ddd;
21 | }
22 |
--------------------------------------------------------------------------------
/shadow-cljs.edn:
--------------------------------------------------------------------------------
1 | {:nrepl {:port 9001}
2 | :npm-deps {:install false}
3 | :builds
4 | {:dev
5 | {:target :browser
6 | :devtools {:watch-dir "resources/public" ; live reload CSS
7 | :hud #{:errors :progress}
8 | :ignore-warnings true} ; warnings don't prevent hot-reload
9 | :output-dir "resources/public/js"
10 | :asset-path "/js"
11 | :modules {:main {:entries [user]
12 | :init-fn user/start!}}
13 | :build-hooks [(hyperfiddle.electric.shadow-cljs.hooks/reload-clj)
14 | (shadow.cljs.build-report/hook {:output-to "target/build_report.html"})
15 | (user/rcf-shadow-hook)]}
16 | :prod
17 | {:target :browser
18 | :output-dir "resources/public/js"
19 | :asset-path "/js"
20 | :module-hash-names true
21 | :modules {:main {:entries [user] :init-fn user/start!}}}}}
22 |
--------------------------------------------------------------------------------
/src-build/build.clj:
--------------------------------------------------------------------------------
1 | (ns build
2 | "build electric.jar library artifact and demos"
3 | (:require [clojure.tools.build.api :as b]
4 | [org.corfield.build :as bb]
5 | [clojure.java.shell :as sh]))
6 |
7 | (def lib 'com.hyperfiddle/electric)
8 | (def version (b/git-process {:git-args "describe --tags --long --always --dirty"}))
9 | (def basis (b/create-basis {:project "deps.edn"}))
10 |
11 | (defn clean [opts]
12 | (bb/clean opts))
13 |
14 | (def class-dir "target/classes")
15 | (defn default-jar-name [{:keys [version] :or {version version}}]
16 | (format "target/%s-%s-standalone.jar" (name lib) version))
17 |
18 | (defn clean-cljs [_]
19 | (b/delete {:path "resources/public/js"}))
20 |
21 | (defn build-client [{:keys [optimize debug verbose version]
22 | :or {optimize true, debug false, verbose false, version version}}]
23 | (println "Building client. Version:" version)
24 | ; this command must run the same on: local machine, docker build, github actions.
25 | ; shell out to let Shadow manage the classpath, it's not obvious how to set that up.
26 | ; This is the only stable way we know to do it.
27 | (let [command (->> ["clj" "-J-Xss4M" "-M:shadow-cljs" "release" "prod"
28 | (when debug "--debug")
29 | (when verbose "--verbose")
30 | "--config-merge"
31 | (pr-str {:compiler-options {:optimizations (if optimize :advanced :simple)}
32 | :closure-defines {'hyperfiddle.electric-client/ELECTRIC_USER_VERSION version}})]
33 | (remove nil?))]
34 | (apply println "Running:" command)
35 | (let [{:keys [exit out err]} (apply sh/sh command)]
36 | (when err (println err))
37 | (when out (println out))
38 | (when-not (zero? exit)
39 | (println "Exit code" exit)
40 | (System/exit exit)))))
41 |
42 | (defn uberjar [{:keys [jar-name version optimize debug verbose]
43 | :or {version version, optimize true, debug false, verbose false}}]
44 | (println "Cleaning up before build")
45 | (clean nil)
46 |
47 | (println "Cleaning cljs compiler output")
48 | (clean-cljs nil)
49 |
50 | (build-client {:optimize optimize, :debug debug, :verbose verbose, :version version})
51 |
52 | (println "Bundling sources")
53 | (b/copy-dir {:src-dirs ["src" "resources"]
54 | :target-dir class-dir})
55 |
56 | (println "Compiling server. Version:" version)
57 | (b/compile-clj {:basis basis
58 | :src-dirs ["src"]
59 | :ns-compile '[prod]
60 | :class-dir class-dir})
61 |
62 | (println "Building uberjar")
63 | (b/uber {:class-dir class-dir
64 | :uber-file (str (or jar-name (default-jar-name {:version version})))
65 | :basis basis
66 | :main 'prod}))
67 |
68 | (defn noop [_]) ; run to preload mvn deps
--------------------------------------------------------------------------------
/src-build/build.md:
--------------------------------------------------------------------------------
1 | # Continuous deployment of demos
2 |
3 | ```shell
4 | clojure -T:build build-client # optimized release build
5 | HYPERFIDDLE_ELECTRIC_APP_VERSION=`git describe --tags --long --always --dirty`
6 | docker build --progress=plain --build-arg VERSION=$HYPERFIDDLE_ELECTRIC_APP_VERSION -t electric-examples-app:latest .
7 | docker run --rm -p 7070:8080 electric-examples-app:latest
8 | # flyctl launch ... ? create fly app, generate fly.toml, see dashboard
9 | # https://fly.io/apps/electric-starter-app
10 |
11 | NO_COLOR=1 flyctl deploy --build-arg VERSION=$HYPERFIDDLE_ELECTRIC_APP_VERSION
12 | # https://electric-starter-app.fly.dev/
13 | ```
14 |
15 | - `NO_COLOR=1` disables docker-cli fancy shell GUI, so that we see the full log (not paginated) in case of exception
16 | - `--build-only` tests the build on fly.io without deploying
17 |
--------------------------------------------------------------------------------
/src/contrib/datafy_fs.clj:
--------------------------------------------------------------------------------
1 | (ns contrib.datafy-fs
2 | "nav implementation for java file system traversals"
3 | (:require [clojure.core.protocols :as ccp :refer [nav]]
4 | [clojure.datafy :refer [datafy]]
5 | [clojure.spec.alpha :as s]
6 | [hyperfiddle.rcf :refer [tests]])
7 | (:import [java.nio.file Path Paths Files]
8 | java.io.File
9 | java.nio.file.LinkOption
10 | [java.nio.file.attribute BasicFileAttributes FileTime]))
11 |
12 | ; spec the data, not the object
13 | (s/def ::name string?)
14 | (s/def ::absolute-path string?)
15 | (s/def ::modified inst?)
16 | (s/def ::created inst?)
17 | (s/def ::accessed inst?)
18 | (s/def ::size string?)
19 | ;; (s/def ::kind (s/nilable qualified-keyword?))
20 | (s/def ::kind qualified-keyword?) ;; HACK FIXME implement nilable in hyperfiddle.spec
21 | (s/def ::file (s/keys :opt [::name ::absolute-path ::modified ::created ::accessed ::size ::kind]))
22 | (s/def ::children (s/coll-of ::file))
23 |
24 | (defn get-extension [?path]
25 | (when ?path
26 | (when-not (= \. (first ?path)) ; hidden
27 | (some-> (last (re-find #"(\.[a-zA-Z0-9]+)$" ?path))
28 | (subs 1)))))
29 |
30 | (tests
31 | "get-extension"
32 | (get-extension nil) := nil
33 | (get-extension "") := nil
34 | (get-extension ".") := nil
35 | (get-extension "..") := nil
36 | (get-extension "image") := nil
37 | (get-extension "image.") := nil
38 | (get-extension "image..") := nil
39 | (get-extension "image.png") := "png"
40 | (get-extension "image.blah.png") := "png"
41 | (get-extension "image.blah..png") := "png"
42 | (get-extension ".gitignore") := nil)
43 |
44 | (comment
45 | "java.io.File interop"
46 | (def h (clojure.java.io/file "node_modules/"))
47 |
48 | (sort (.listFiles h))
49 |
50 | (.getName h) := "src"
51 | (.getPath h) := "src"
52 | (.isDirectory h) := true
53 | (.isFile h) := false
54 | ;(.getParent h) := nil -- ??
55 | ;(.getParentFile h) := nil -- ??
56 | (-> (datafy java.io.File) :members keys)
57 | (->> (seq (.listFiles h)) (take 1) first datafy)
58 | (for [x (take 5 (.listFiles h))] (.getName x)))
59 |
60 | (defn file-path "get java.nio.file.Path of j.n.f.File"
61 | [^File f] (-> f .getAbsolutePath (java.nio.file.Paths/get (make-array String 0))))
62 |
63 | (tests
64 | (def p (file-path (clojure.java.io/file "src")))
65 | (instance? Path p) := true
66 | (-> (datafy Path) :members keys)
67 | (-> p .getRoot str) := "/"
68 | (-> p .getFileName str) := "src"
69 | (-> p .getParent .getFileName str) := "electric"
70 | (-> p .getParent .toFile .getName) := "electric"
71 | #_(-> p .getParent .toFile datafy))
72 |
73 | (defn path-attrs [^Path p]
74 | (Files/readAttributes p BasicFileAttributes (make-array java.nio.file.LinkOption 0)))
75 |
76 | (tests
77 | (def attrs (path-attrs (file-path (clojure.java.io/file "src"))))
78 | (instance? BasicFileAttributes attrs) := true
79 | (.isDirectory attrs) := true
80 | (.isSymbolicLink attrs) := false
81 | (.isRegularFile attrs) := false
82 | (.isOther attrs) := false)
83 |
84 | (defn file-attrs [^File f] (path-attrs (file-path f)))
85 |
86 | (tests
87 | (file-attrs (clojure.java.io/file "src"))
88 | )
89 |
90 | (def ... `...) ; define a value for easy test assertions
91 |
92 | (extend-protocol ccp/Datafiable
93 | java.nio.file.attribute.FileTime
94 | (datafy [o] (-> o .toInstant java.util.Date/from)))
95 |
96 | (extend-protocol ccp/Datafiable
97 | java.io.File
98 | (datafy [^File f]
99 | ; represent object's top layer as EDN-ready value records, for display
100 | ; datafy is partial display view of an object as value records
101 | ; nav is ability to resolve back to the underlying object pointers
102 | ; they compose to navigate display views of objects like a link
103 | (let [attrs (file-attrs f)
104 | n (.getName f)]
105 | (as-> {::name n
106 | ::kind (cond (.isDirectory attrs) ::dir
107 | (.isSymbolicLink attrs) ::symlink
108 | (.isOther attrs) ::other
109 | (.isRegularFile attrs) (if-let [s (get-extension n)]
110 | (keyword (namespace ::foo) s)
111 | ::unknown-kind)
112 | () ::unknown-kind)
113 | ::absolute-path (-> f .getAbsolutePath)
114 | ::created (-> attrs .creationTime .toInstant java.util.Date/from)
115 | ::accessed (-> attrs .lastAccessTime .toInstant java.util.Date/from)
116 | ::modified (-> attrs .lastModifiedTime .toInstant java.util.Date/from)
117 | ::size (.size attrs)} %
118 | (merge % (if (= ::dir (::kind %))
119 | {::children (lazy-seq (sort (.listFiles f)))
120 | ::parent `...}))
121 | (with-meta % {`ccp/nav
122 | (fn [xs k v]
123 | (case k
124 | ; reverse data back to object, to be datafied again by caller
125 | ::modified (.lastModifiedTime attrs)
126 | ::created (.creationTime attrs)
127 | ::accessed (.lastAccessTime attrs)
128 | ::children (some-> v vec)
129 | ::parent (-> f file-path .getParent .toFile)
130 | v))})))))
131 |
132 | (tests
133 | ; careful, calling seq loses metas on the underlying
134 | (def h (clojure.java.io/file "src/"))
135 | (type h) := java.io.File
136 | "(datafy file) returns an EDN-ready data view that is one layer deep"
137 | (datafy h)
138 | := #:user.datafy-fs{:name "src",
139 | :absolute-path _,
140 | :size _,
141 | :modified _,
142 | :created _,
143 | :accessed _,
144 | :kind ::dir,
145 | :children _
146 | :parent ...})
147 |
148 | (tests
149 | "datafy of a directory includes a Clojure coll of children, but child elements are native file
150 | objects"
151 | (as-> (datafy h) %
152 | (nav % ::children (::children %))
153 | (datafy %)
154 | (take 2 (map type %)))
155 | := [java.io.File java.io.File]
156 |
157 | "nav to a leaf returns the native object"
158 | (as-> (datafy h) %
159 | (nav % ::modified (::modified %)))
160 | (type *1) := java.nio.file.attribute.FileTime
161 |
162 | "datafy again to get the plain value"
163 | (type (datafy *2)) := java.util.Date)
164 |
165 | (tests
166 | (as-> (datafy h) %
167 | (nav % ::children (::children %))
168 | (datafy %) ; can skip - simple data
169 | (map datafy %)
170 | (vec (filter #(= (::name %) "hyperfiddle") %)) ; stabilize test
171 | (nav % 0 (% 0))
172 | (datafy %)
173 | #_(s/conform ::file %))
174 | := #:user.datafy-fs{:name "hyperfiddle",
175 | :absolute-path _,
176 | :size _,
177 | :modified _,
178 | :created _,
179 | :accessed _,
180 | :kind ::dir,
181 | :children _
182 | :parent ...})
183 |
184 | (tests
185 | "nav into children and back up via parent ref"
186 | (def m (datafy h))
187 | (::name m) := "src"
188 | (as-> m %
189 | (nav % ::children (::children %))
190 | (datafy %) ; dir
191 | (nav % 0 (get % 0)) ; first file in dir
192 | (datafy %)
193 | (nav % ::parent (::parent %)) ; dir (skip level on way up)
194 | (datafy %)
195 | (::name %))
196 | := "src")
197 |
198 | (defn absolute-path [^String path-str & more]
199 | (str (.toAbsolutePath (java.nio.file.Path/of ^String path-str (into-array String more)))))
200 |
201 | (comment
202 | (absolute-path "./")
203 | (absolute-path "node_modules")
204 | (clojure.java.io/file (absolute-path "./"))
205 | (clojure.java.io/file (absolute-path "node_modules")))
206 |
207 | (s/fdef list-files :args (s/cat :file any?) :ret (s/coll-of any?))
208 | (defn list-files [^String path-str]
209 | (try (let [m (datafy (clojure.java.io/file path-str))]
210 | (nav m ::children (::children m)))
211 | (catch java.nio.file.NoSuchFileException _)))
212 |
213 | (comment
214 | (list-files (absolute-path "./"))
215 | (list-files (absolute-path "node_modules")))
--------------------------------------------------------------------------------
/src/dev.cljc:
--------------------------------------------------------------------------------
1 | (ns dev
2 | (:require
3 | [datascript.core :as d]
4 | [hyperfiddle.api :as hf]
5 | [hyperfiddle.rcf :refer [tests % tap]]))
6 |
7 |
8 | (defn fixtures [$]
9 | ; portable
10 | (-> $
11 | (d/with [{:db/id 1 :order/type :order/gender :db/ident :order/male}
12 | {:db/id 2 :order/type :order/gender :db/ident :order/female}])
13 | :db-after
14 | (d/with [{:db/id 3 :order/type :order/shirt-size :db/ident :order/mens-small :order/gender :order/male}
15 | {:db/id 4 :order/type :order/shirt-size :db/ident :order/mens-medium :order/gender :order/male}
16 | {:db/id 5 :order/type :order/shirt-size :db/ident :order/mens-large :order/gender :order/male}
17 | {:db/id 6 :order/type :order/shirt-size :db/ident :order/womens-small :order/gender :order/female}
18 | {:db/id 7 :order/type :order/shirt-size :db/ident :order/womens-medium :order/gender :order/female}
19 | {:db/id 8 :order/type :order/shirt-size :db/ident :order/womens-large :order/gender :order/female}])
20 | :db-after
21 | (d/with [{:db/id 9 :order/email "alice@example.com" :order/gender :order/female :order/shirt-size :order/womens-large
22 | :order/tags [:a :b :c]}
23 | {:db/id 10 :order/email "bob@example.com" :order/gender :order/male :order/shirt-size :order/mens-large
24 | :order/tags [:b]}
25 | {:db/id 11 :order/email "charlie@example.com" :order/gender :order/male :order/shirt-size :order/mens-medium}])
26 | :db-after
27 | #_(d/with [{:db/id 12 :order/email "alice@example.com" :order/gender :order/female :order/shirt-size :order/womens-large}
28 | {:order/email "bob@example.com" :order/gender :order/male :order/shirt-size :order/mens-large}
29 | {:order/email "charlie@example.com" :order/gender :order/male :order/shirt-size :order/mens-medium}])
30 |
31 | #_:db-after))
32 |
33 | ;(defn init-datomic []
34 | ; (def schema [{:db/ident :order/email :db/valueType :db.type/string :db/cardinality :db.cardinality/one :db/unique :db.unique/identity}
35 | ; {:db/ident :order/gender :db/valueType :db.type/ref :db/cardinality :db.cardinality/one}
36 | ; {:db/ident :order/shirt-size :db/valueType :db.type/ref :db/cardinality :db.cardinality/one}
37 | ; {:db/ident :order/type :db/valueType :db.type/keyword :db/cardinality :db.cardinality/one}
38 | ; {:db/ident :order/tags :db/valueType :db.type/keyword :db/cardinality :db.cardinality/many}])
39 | ; (d/create-database "datomic:mem://hello-world")
40 | ; (def ^:dynamic *$* (-> (d/connect "datomic:mem://hello-world") d/db (d/with schema) :db-after fixtures))
41 | ; #_(alter-var-root #'*$* (constantly $)))
42 |
43 | (def schema
44 | ;; manual db/ids for tests consistency and clarity, not a requirement
45 | [{:db/id 100001 :db/ident :order/email, :db/valueType :db.type/string, :db/cardinality :db.cardinality/one, :db/unique :db.unique/identity}
46 | {:db/id 100002 :db/ident :order/gender, :db/valueType :db.type/ref :db/cardinality :db.cardinality/one}
47 | {:db/id 100003 :db/ident :order/shirt-size, :db/valueType :db.type/ref :db/cardinality :db.cardinality/one}
48 | {:db/id 100004 :db/ident :order/type, :db/valueType :db.type/keyword :db/cardinality :db.cardinality/one}
49 | {:db/id 100005 :db/ident :order/tags, :db/valueType :db.type/keyword :db/cardinality :db.cardinality/many}])
50 |
51 | (def db-config {:store {:backend :mem, :id "default"}})
52 |
53 | (defn setup-db! []
54 | ;; FIXME Datascript doesn’t support :db/valueType, using :hf/valueType in the meantime
55 | (let [-schema {:order/email {:hf/valueType :db.type/string :db/cardinality :db.cardinality/one :db/unique :db.unique/identity}
56 | :order/gender {:db/valueType :db.type/ref :db/cardinality :db.cardinality/one}
57 | :order/shirt-size {:db/valueType :db.type/ref :db/cardinality :db.cardinality/one}
58 | :order/type {#_#_:db/valueType :db.type/keyword :db/cardinality :db.cardinality/one}
59 | :order/tags {#_#_:db/valueType :db.type/keyword :db/cardinality :db.cardinality/many}
60 | :db/ident {:db/unique :db.unique/identity, :hf/valueType :db.type/keyword}}]
61 | #?(:clj (alter-var-root #'schema (constantly -schema))
62 | :cljs (set! schema -schema)))
63 | ;(log/info "Initializing Test Database")
64 | (def conn (d/create-conn schema))
65 | (let [$ (-> conn d/db fixtures)]
66 | #?(:clj (alter-var-root #'hf/*$* (constantly $))
67 | :cljs (set! hf/*$* $))))
68 |
69 | #?(:clj (setup-db!))
70 |
71 | (def db hf/*$*) ; for @(requiring-resolve 'dev/db)
72 |
73 | (def male 1 #_:order/male #_17592186045418)
74 | (def female 2 #_:order/female #_17592186045419)
75 | (def m-sm 3 #_17592186045421)
76 | (def m-md 4 #_nil)
77 | (def m-lg 5 #_nil)
78 | (def w-sm 6 #_nil)
79 | (def w-md 7 #_nil)
80 | (def w-lg 8 #_nil)
81 | (def alice 9 #_17592186045428)
82 | (def bob 10 #_nil)
83 | (def charlie 11 #_nil)
84 |
85 | (tests
86 | (def e [:order/email "alice@example.com"])
87 |
88 | (tests
89 | "(d/pull ['*]) is best for tests"
90 | (d/pull db ['*] e)
91 | := {:db/id 9,
92 | :order/email "alice@example.com",
93 | :order/shirt-size #:db{:id 8},
94 | :order/gender #:db{:id 2}
95 | :order/tags [:a :b :c]})
96 |
97 | (comment #_tests
98 | "careful, entity type is not= to equivalent hashmap"
99 | (d/touch (d/entity db e))
100 | ; expected failure
101 | := {:order/email "alice@example.com",
102 | :order/gender #:db{:id 2},
103 | :order/shirt-size #:db{:id 8},
104 | :order/tags #{:c :b :a},
105 | :db/id 9})
106 |
107 | (tests
108 | "entities are not maps"
109 | (type (d/touch (d/entity db e)))
110 | (type *1) := datascript.impl.entity.Entity) ; not a map
111 |
112 | (tests
113 | "careful, entity API tests are fragile and (into {}) is insufficient"
114 | (->> (d/touch (d/entity db e)) ; touch is the best way to inspect an entity
115 | (into {})) ; but it's hard to convert to a map...
116 | := #:order{#_#_:id 9 ; db/id is not present!
117 | :email "alice@example.com",
118 | :gender _ #_#:db{:id 2}, ; entity ref not =
119 | :shirt-size _ #_#:db{:id 8}, ; entity ref not =
120 | :tags #{:c :b :a}}
121 |
122 | "select keys doesn't fix the problem as it's not recursive"
123 | (-> (d/touch (d/entity db e))
124 | (select-keys [:order/email :order/shirt-size :order/gender]))
125 | := #:order{:email "alice@example.com",
126 | :shirt-size _ #_#:db{:id 8}, ; still awkward, need recursive pull
127 | :gender _ #_#:db{:id 2}})
128 |
129 | "TLDR is use (d/pull ['*]) like the first example"
130 | (tests
131 | (d/pull db ['*] :order/female)
132 | := {:db/id female :db/ident :order/female :order/type :order/gender})
133 |
134 | (tests
135 | (d/q '[:find [?e ...] :where [_ :order/gender ?e]] db)
136 | := [2 1] #_[:order/male :order/female])
137 | )
138 |
139 | (comment
140 | "CI tests"
141 | #?(:clj (alter-var-root #'hyperfiddle.rcf/*generate-tests* (constantly false)))
142 | (hyperfiddle.rcf/enable!)
143 | (require 'clojure.test)
144 | (clojure.test/run-all-tests #"(hyperfiddle.api|user.orders)"))
145 |
146 | (comment
147 | "Performance profiling, use :profile deps alias"
148 | (require '[clj-async-profiler.core :as prof])
149 | (prof/serve-files 8082)
150 | ;; Navigate to http://localhost:8082
151 | (prof/start {:framebuf 10000000})
152 | (prof/stop))
153 |
--------------------------------------------------------------------------------
/src/electric_server_java11_jetty10.clj:
--------------------------------------------------------------------------------
1 | ;; Start from this example if you don’t need Java 8 compat.
2 | ;; See `deps.edn`.
3 | (ns electric-server-java11-jetty10
4 | (:require [clojure.java.io :as io]
5 | [hyperfiddle.electric-jetty-adapter :as adapter]
6 | [clojure.tools.logging :as log]
7 | [ring.adapter.jetty9 :as ring]
8 | [ring.middleware.basic-authentication :as auth]
9 | [ring.middleware.content-type :refer [wrap-content-type]]
10 | [ring.middleware.cookies :as cookies]
11 | [ring.middleware.params :refer [wrap-params]]
12 | [ring.middleware.resource :refer [wrap-resource]]
13 | [ring.util.response :as res]
14 | [clojure.string :as str]
15 | [clojure.edn :as edn])
16 | (:import [java.io IOException]
17 | [java.net BindException]
18 | [org.eclipse.jetty.server.handler.gzip GzipHandler]))
19 |
20 | (defn authenticate [username password] username) ; demo (accept-all) authentication
21 |
22 | (defn wrap-demo-authentication "A Basic Auth example. Accepts any username/password and store the username in a cookie."
23 | [next-handler]
24 | (-> (fn [ring-req]
25 | (let [res (next-handler ring-req)]
26 | (if-let [username (:basic-authentication ring-req)]
27 | (res/set-cookie res "username" username {:http-only true})
28 | res)))
29 | (cookies/wrap-cookies)
30 | (auth/wrap-basic-authentication authenticate)))
31 |
32 | (defn wrap-demo-router "A basic path-based routing middleware"
33 | [next-handler]
34 | (fn [ring-req]
35 | (case (:uri ring-req)
36 | "/auth" (let [response ((wrap-demo-authentication next-handler) ring-req)]
37 | (if (= 401 (:status response)) ; authenticated?
38 | response ; send response to trigger auth prompt
39 | (-> (res/status response 302) ; redirect
40 | (res/header "Location" (get-in ring-req [:headers "referer"]))))) ; redirect to where the auth originated
41 | ;; For any other route, delegate to next middleware
42 | (next-handler ring-req))))
43 |
44 | (defn template "Takes a `string` and a map of key-values `kvs`. Replace all instances of `$key$` by value in `string`"
45 | [string kvs]
46 | (reduce-kv (fn [r k v] (str/replace r (str "$" k "$") v)) string kvs))
47 |
48 | (defn get-modules [manifest-path]
49 | (when-let [manifest (io/resource manifest-path)]
50 | (let [manifest-folder (when-let [folder-name (second (rseq (str/split manifest-path #"\/")))]
51 | (str "/" folder-name "/"))]
52 | (->> (slurp manifest)
53 | (edn/read-string)
54 | (reduce (fn [r module] (assoc r (keyword "hyperfiddle.client.module" (name (:name module))) (str manifest-folder (:output-name module)))) {})))))
55 |
56 | (defn wrap-index-page
57 | "Server the `index.html` file with injected javascript modules from `manifest.edn`. `manifest.edn` is generated by the client build and contains javascript modules information."
58 | [next-handler resources-path manifest-path]
59 | (fn [ring-req]
60 | (if-let [response (res/resource-response (str resources-path "/index.html"))]
61 | (if-let [modules (get-modules manifest-path)]
62 | (-> (res/response (template (slurp (:body response)) modules)) ; TODO cache in prod mode
63 | (res/content-type "text/html") ; ensure `index.html` is not cached
64 | (res/header "Cache-Control" "no-store")
65 | (res/header "Last-Modified" (get-in response [:headers "Last-Modified"])))
66 | ;; No manifest found, can't inject js modules
67 | (-> (res/not-found "Missing client program manifest")
68 | (res/content-type "text/plain")))
69 | ;; index.html file not found on classpath
70 | (next-handler ring-req))))
71 |
72 | (def VERSION (not-empty (System/getProperty "ELECTRIC_USER_VERSION"))) ; see Dockerfile
73 |
74 | (defn wrap-reject-stale-client
75 | "Intercept websocket UPGRADE request and check if client and server versions matches.
76 | An electric client is allowed to connect if its version matches the server's version, or if the server doesn't have a version set (dev mode).
77 | Otherwise, the client connection is rejected gracefully."
78 | [next-handler]
79 | (fn [ring-req]
80 | (if (ring/ws-upgrade-request? ring-req)
81 | (let [client-version (get-in ring-req [:query-params "ELECTRIC_USER_VERSION"])]
82 | (cond
83 | (nil? VERSION) (next-handler ring-req)
84 | (= client-version VERSION) (next-handler ring-req)
85 | :else (adapter/reject-websocket-handler 1008 "stale client") ; https://www.rfc-editor.org/rfc/rfc6455#section-7.4.1
86 | ))
87 | (next-handler ring-req))))
88 |
89 | (defn wrap-electric-websocket [next-handler]
90 | (fn [ring-request]
91 | (if (ring/ws-upgrade-request? ring-request)
92 | (let [authenticated-request (auth/basic-authentication-request ring-request authenticate) ; optional
93 | electric-message-handler (partial adapter/electric-ws-message-handler authenticated-request)] ; takes the ring request as first arg - makes it available to electric program
94 | (ring/ws-upgrade-response (adapter/electric-ws-adapter electric-message-handler)))
95 | (next-handler ring-request))))
96 |
97 | (defn electric-websocket-middleware [next-handler]
98 | (-> (wrap-electric-websocket next-handler) ; 4. connect electric client
99 | (cookies/wrap-cookies) ; 3. makes cookies available to Electric app
100 | (wrap-reject-stale-client) ; 2. reject stale electric client
101 | (wrap-params) ; 1. parse query params
102 | ))
103 |
104 | (defn not-found-handler [_ring-request]
105 | (-> (res/not-found "Not found")
106 | (res/content-type "text/plain")))
107 |
108 | (defn http-middleware [resources-path manifest-path]
109 | ;; these compose as functions, so are applied bottom up
110 | (-> not-found-handler
111 | (wrap-index-page resources-path manifest-path) ; 5. otherwise fallback to default page file
112 | (wrap-resource resources-path) ; 4. serve static file from classpath
113 | (wrap-content-type) ; 3. detect content (e.g. for index.html)
114 | (wrap-demo-router) ; 2. route
115 | (electric-websocket-middleware) ; 1. intercept electric websocket
116 | ))
117 |
118 | (defn- add-gzip-handler
119 | "Makes Jetty server compress responses. Optional but recommended."
120 | [server]
121 | (.setHandler server
122 | (doto (GzipHandler.)
123 | #_(.setIncludedMimeTypes (into-array ["text/css" "text/plain" "text/javascript" "application/javascript" "application/json" "image/svg+xml"])) ; only compress these
124 | (.setMinGzipSize 1024)
125 | (.setHandler (.getHandler server)))))
126 |
127 | (defn start-server! [{:keys [port resources-path manifest-path]
128 | :or {port 8080
129 | resources-path "public"
130 | manifest-path "public/js/manifest.edn"}
131 | :as config}]
132 | (try
133 | (let [server (ring/run-jetty (http-middleware resources-path manifest-path)
134 | (merge {:port port
135 | :join? false
136 | :configurator add-gzip-handler}
137 | config))
138 | final-port (-> server (.getConnectors) first (.getPort))]
139 | (println "\n👉 App server available at" (str "http://" (:host config) ":" final-port "\n"))
140 | server)
141 |
142 | (catch IOException err
143 | (if (instance? BindException (ex-cause err)) ; port is already taken, retry with another one
144 | (do (log/warn "Port" port "was not available, retrying with" (inc port))
145 | (start-server! (update config :port inc)))
146 | (throw err)))))
147 |
148 |
--------------------------------------------------------------------------------
/src/electric_server_java8_jetty9.clj:
--------------------------------------------------------------------------------
1 | ;; Start from this example if you need Java 8 compat.
2 | ;; See `deps.edn`
3 | (ns electric-server-java8-jetty9
4 | (:require [clojure.java.io :as io]
5 | [hyperfiddle.electric-jetty-adapter :as adapter]
6 | [clojure.tools.logging :as log]
7 | [ring.adapter.jetty9 :as ring]
8 | [ring.middleware.basic-authentication :as auth]
9 | [ring.middleware.content-type :refer [wrap-content-type]]
10 | [ring.middleware.cookies :as cookies]
11 | [ring.middleware.params :refer [wrap-params]]
12 | [ring.middleware.resource :refer [wrap-resource]]
13 | [ring.util.response :as res]
14 | [clojure.string :as str]
15 | [clojure.edn :as edn])
16 | (:import [java.io IOException]
17 | [java.net BindException]
18 | [org.eclipse.jetty.server.handler.gzip GzipHandler]))
19 |
20 | (defn authenticate [username password] username) ; demo (accept-all) authentication
21 |
22 | (defn wrap-demo-authentication "A Basic Auth example. Accepts any username/password and store the username in a cookie."
23 | [next-handler]
24 | (-> (fn [ring-req]
25 | (let [res (next-handler ring-req)]
26 | (if-let [username (:basic-authentication ring-req)]
27 | (res/set-cookie res "username" username {:http-only true})
28 | res)))
29 | (cookies/wrap-cookies)
30 | (auth/wrap-basic-authentication authenticate)))
31 |
32 | (defn wrap-demo-router "A basic path-based routing middleware"
33 | [next-handler]
34 | (fn [ring-req]
35 | (case (:uri ring-req)
36 | "/auth" (let [response ((wrap-demo-authentication next-handler) ring-req)]
37 | (if (= 401 (:status response)) ; authenticated?
38 | response ; send response to trigger auth prompt
39 | (-> (res/status response 302) ; redirect
40 | (res/header "Location" (get-in ring-req [:headers "referer"]))))) ; redirect to where the auth originated
41 | ;; For any other route, delegate to next middleware
42 | (next-handler ring-req))))
43 |
44 | (defn template "Takes a `string` and a map of key-values `kvs`. Replace all instances of `$key$` by value in `string`"
45 | [string kvs]
46 | (reduce-kv (fn [r k v] (str/replace r (str "$" k "$") v)) string kvs))
47 |
48 | (defn get-modules [manifest-path]
49 | (when-let [manifest (io/resource manifest-path)]
50 | (let [manifest-folder (when-let [folder-name (second (rseq (str/split manifest-path #"\/")))]
51 | (str "/" folder-name "/"))]
52 | (->> (slurp manifest)
53 | (edn/read-string)
54 | (reduce (fn [r module] (assoc r (keyword "hyperfiddle.client.module" (name (:name module))) (str manifest-folder (:output-name module)))) {})))))
55 |
56 | (defn wrap-index-page
57 | "Server the `index.html` file with injected javascript modules from `manifest.edn`. `manifest.edn` is generated by the client build and contains javascript modules information."
58 | [next-handler resources-path manifest-path]
59 | (fn [ring-req]
60 | (if-let [response (res/resource-response (str resources-path "/index.html"))]
61 | (if-let [modules (get-modules manifest-path)]
62 | (-> (res/response (template (slurp (:body response)) modules)) ; TODO cache in prod mode
63 | (res/content-type "text/html") ; ensure `index.html` is not cached
64 | (res/header "Cache-Control" "no-store")
65 | (res/header "Last-Modified" (get-in response [:headers "Last-Modified"])))
66 | ;; No manifest found, can't inject js modules
67 | (-> (res/not-found "Missing client program manifest")
68 | (res/content-type "text/plain")))
69 | ;; index.html file not found on classpath
70 | (next-handler ring-req))))
71 |
72 | (def VERSION (not-empty (System/getProperty "ELECTRIC_USER_VERSION"))) ; see Dockerfile
73 |
74 | (defn wrap-reject-stale-client
75 | "Intercept websocket UPGRADE request and check if client and server versions matches.
76 | An electric client is allowed to connect if its version matches the server's version, or if the server doesn't have a version set (dev mode).
77 | Otherwise, the client connection is rejected gracefully."
78 | [next-handler]
79 | (fn [ring-req]
80 | (let [client-version (get-in ring-req [:query-params "ELECTRIC_USER_VERSION"])]
81 | (cond
82 | (nil? VERSION) (next-handler ring-req)
83 | (= client-version VERSION) (next-handler ring-req)
84 | :else (adapter/reject-websocket-handler 1008 "stale client") ; https://www.rfc-editor.org/rfc/rfc6455#section-7.4.1
85 | ))))
86 |
87 | (def websocket-middleware
88 | (fn [next-handler]
89 | (-> (cookies/wrap-cookies next-handler) ; makes cookies available to Electric app
90 | (wrap-reject-stale-client)
91 | (wrap-params))))
92 |
93 | (defn not-found-handler [_ring-request]
94 | (-> (res/not-found "Not found")
95 | (res/content-type "text/plain")))
96 |
97 | (defn http-middleware [resources-path manifest-path]
98 | ;; these compose as functions, so are applied bottom up
99 | (-> not-found-handler
100 | (wrap-index-page resources-path manifest-path) ; 4. otherwise fallback to default page file
101 | (wrap-resource resources-path) ; 3. serve static file from classpath
102 | (wrap-content-type) ; 2. detect content (e.g. for index.html)
103 | (wrap-demo-router) ; 1. route
104 | ))
105 |
106 | (defn- add-gzip-handler
107 | "Makes Jetty server compress responses. Optional but recommended."
108 | [server]
109 | (.setHandler server
110 | (doto (GzipHandler.)
111 | #_(.setIncludedMimeTypes (into-array ["text/css" "text/plain" "text/javascript" "application/javascript" "application/json" "image/svg+xml"])) ; only compress these
112 | (.setMinGzipSize 1024)
113 | (.setHandler (.getHandler server)))))
114 |
115 | (defn start-server! [entrypoint {:keys [port resources-path manifest-path]
116 | :or {port 8080
117 | resources-path "public"
118 | manifest-path "public/js/manifest.edn"}
119 | :as config}]
120 | (try
121 | (let [server (ring/run-jetty (http-middleware resources-path manifest-path)
122 | (merge {:port port
123 | :join? false
124 | :configurator add-gzip-handler
125 | ;; Jetty 9 forces us to declare WS paths out of a ring handler.
126 | ;; For Jetty 10 (NOT Java 8 compatible), drop the following and use `wrap-electric-websocket` as above
127 | :websockets {"/" (websocket-middleware
128 | (fn [ring-req]
129 | (adapter/electric-ws-adapter
130 | (partial adapter/electric-ws-message-handler
131 | (auth/basic-authentication-request ring-req authenticate)
132 | entrypoint))))}}
133 | config))
134 | final-port (-> server (.getConnectors) first (.getPort))]
135 | (println "\n👉 App server available at" (str "http://" (:host config) ":" final-port "\n"))
136 | server)
137 |
138 | (catch IOException err
139 | (if (instance? BindException (ex-cause err)) ; port is already taken, retry with another one
140 | (do (log/warn "Port" port "was not available, retrying with" (inc port))
141 | (start-server! entrypoint (update config :port inc)))
142 | (throw err)))))
143 |
--------------------------------------------------------------------------------
/src/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | %highlight(%-5level) %logger: %msg%n
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/prod.clj:
--------------------------------------------------------------------------------
1 | (ns prod
2 | (:gen-class)
3 | (:require user-main ; in prod, load app into server so it can accept clients
4 | [hyperfiddle.electric :as e]
5 | electric-server-java8-jetty9))
6 |
7 | (def electric-server-config
8 | {:host "0.0.0.0", :port 8080, :resources-path "public"})
9 |
10 | (defn -main [& args] ; run with `clj -M -m prod`
11 | (electric-server-java8-jetty9/start-server!
12 | (fn [ring-req] (e/boot-server {} user-main/Main ring-req))
13 | electric-server-config))
14 |
15 | ; On CLJS side we reuse src/user.cljs for prod entrypoint
--------------------------------------------------------------------------------
/src/test.clj:
--------------------------------------------------------------------------------
1 | (ns test
2 | (:require [contrib.datomic-contrib :as dx]
3 | [contrib.datomic-m :as d]
4 | [missionary.core :as m]
5 | [hyperfiddle.rcf :refer [tests]]
6 | test.mbrainz
7 | test.seattle
8 | test.person-model))
9 |
10 | (def ^:dynamic datomic-client)
11 | (def ^:dynamic datomic-conn)
12 | (def ^:dynamic datomic-db)
13 | (def ^:dynamic schema)
14 | (def ^:dynamic fixtures []) ; local datomic tx to rebase onto the db
15 |
16 | (def pour-lamour 87960930235113)
17 | (def cobblestone 536561674378709)
18 |
19 | (def datomic-config {:server-type :dev-local :system "datomic-samples"})
20 |
21 | (defn install-test-state []
22 | (alter-var-root #'datomic-client (constantly (d/client datomic-config)))
23 | (assert (some? datomic-client))
24 |
25 | (alter-var-root #'datomic-conn (constantly (m/? (d/connect datomic-client {:db-name "mbrainz-subset"}))))
26 | (assert (some? datomic-conn))
27 |
28 | (alter-var-root #'datomic-db (constantly (:db-after (m/? (d/with (m/? (d/with-db datomic-conn)) fixtures)))))
29 | (assert (some? datomic-db))
30 |
31 | (alter-var-root #'schema (constantly (m/? (dx/schema! datomic-db))))
32 | (assert (some? schema)))
33 |
34 | (tests
35 | (some? schema) := true
36 |
37 | (m/? (d/pull test/datomic-db {:eid pour-lamour :selector ['*]}))
38 | := {:db/id 87960930235113,
39 | :abstractRelease/gid #uuid"f05a1be3-e383-4cd4-ad2a-150ae118f622",
40 | :abstractRelease/name "Pour l’amour des sous / Parle au patron, ma tête est malade",
41 | :abstractRelease/type #:db{:id 35435060739965075, :ident :release.type/single},
42 | :abstractRelease/artists [#:db{:id 20512488927800905}
43 | #:db{:id 68459991991856131}],
44 | :abstractRelease/artistCredit "Jean Yanne & Michel Magne"}
45 |
46 | (m/? (d/pull test/datomic-db {:eid cobblestone :selector ['*]}))
47 | := {:db/id 536561674378709,
48 | :label/gid #uuid"066474f9-fad7-48dd-868f-04d8bb0a5253",
49 | :label/name "Cobblestone",
50 | :label/sortName "Cobblestone",
51 | :label/type #:db{:id 44604987715616963, :ident :label.type/originalProduction},
52 | :label/country #:db{:id 63793664643563930, :ident :country/US},
53 | :label/startYear 1972}
54 | nil)
--------------------------------------------------------------------------------
/src/test/mbrainz.clj:
--------------------------------------------------------------------------------
1 | (ns test.mbrainz)
2 |
3 | ; # Scrap / old stuff
4 | ;
5 | ;* do NOT use mbrainz-1968-1973 (https://github.com/Datomic/mbrainz-importer - don't use this one, too big
6 | ; mbrainz-1968-1973 instructions (don't do this):
7 | ;3. Clone this repo:
8 | ;4. `cd` into it
9 | ;5. create a file `manifest.edn` with this content:
10 | ;```clojure
11 | ;{:client-cfg {:server-type :dev-local
12 | ; :system "datomic-samples"}
13 | ; :db-name "mbrainz-1968-1973"
14 | ; :basedir "subsets"
15 | ; :concurrency 3}
16 | ;```
17 | ;4. In `deps.edn`, set `com.datomic/dev-local` version to `"1.0.243"`
18 | ;5. Run `clojure -M -m datomic.mbrainz.importer manifest.edn`
--------------------------------------------------------------------------------
/src/test/person_model.clj:
--------------------------------------------------------------------------------
1 | (ns test.person-model
2 | (:require [contrib.data :refer [index-by]]))
3 |
4 | (def schema
5 | (->> [{:db/ident :person/name, :db/valueType {:db/ident :db.type/string}, :db/cardinality {:db/ident :db.cardinality/one} :db/unique :db.unique/identity}
6 | {:db/ident :person/liked-tags, :db/valueType {:db/ident :db.type/keyword}, :db/cardinality {:db/ident :db.cardinality/many}}
7 | {:db/ident :employee/manager, :db/valueType {:db/ident :db.type/ref}, :db/cardinality {:db/ident :db.cardinality/one}}
8 | {:db/ident :person/siblings, :db/valueType {:db/ident :db.type/ref}, :db/cardinality {:db/ident :db.cardinality/many}}
9 | {:db/ident :person/address, :db/valueType {:db/ident :db.type/ref}, :db/cardinality {:db/ident :db.cardinality/one}, :db/isComponent true}
10 | {:db/ident :person/summerHomes, :db/valueType {:db/ident :db.type/ref}, :db/cardinality {:db/ident :db.cardinality/many}, :db/isComponent true}
11 | {:db/ident :address/zip, :db/valueType {:db/ident :db.type/string}, :db/cardinality {:db/ident :db.cardinality/one}}
12 | {:db/ident :person/age, :db/valueType {:db/ident :db.type/long}, :db/cardinality {:db/ident :db.cardinality/one}}
13 | {:db/ident :person/bestFriend, :db/valueType {:db/ident :db.type/ref}, :db/cardinality {:db/ident :db.cardinality/one}}
14 | {:db/ident :person/friends, :db/valueType {:db/ident :db.type/ref}, :db/cardinality {:db/ident :db.cardinality/many}}]
15 | (index-by :db/ident)))
16 |
17 | (def bob-map-stmt "covers all combinations"
18 | {:person/name "Bob" ; scalar one
19 | :person/address {:address/zip "12345"} ; ref one component
20 | :person/summerHomes [{:address/zip "11111"} ; ref many component
21 | {:address/zip "22222"}]
22 | :person/liked-tags [:movies :ice-cream :clojure] ; scalar many
23 | :employee/manager {:person/name "Earnest"} ; ref one -> unique
24 | :person/siblings [{:person/name "Cindy"} ; ref many -> unique
25 | {:person/name "David"}]
26 | :person/bestFriend "Benjamin"
27 | :person/friends ["Harry", "Yennefer"]})
28 |
29 | (def bob-txn [bob-map-stmt
30 | [:db/cas 1 :person/age 41 42]
31 | [:user.fn/foo 'x 'y 'z 'q 'r]])
32 |
33 |
--------------------------------------------------------------------------------
/src/test/seattle.clj:
--------------------------------------------------------------------------------
1 | (ns test.seattle
2 | (:require [contrib.data :refer [index-by]]))
3 |
4 | (def schema
5 | (->> [{:db/ident :community/name, :db/valueType :db.type/string, :db/cardinality :db.cardinality/one, :db/fulltext true, :db/doc "A community's name"}
6 | {:db/ident :community/url, :db/valueType :db.type/string, :db/cardinality :db.cardinality/one, :db/doc "A community's url"}
7 | {:db/ident :community/neighborhood, :db/valueType :db.type/ref, :db/cardinality :db.cardinality/one, :db/doc "A community's neighborhood"}
8 | {:db/ident :community/category, :db/valueType :db.type/string, :db/cardinality :db.cardinality/many, :db/fulltext true, :db/doc "All community categories"}
9 | {:db/ident :community/orgtype, :db/valueType :db.type/ref, :db/cardinality :db.cardinality/one, :db/doc "A community orgtype enum value"}
10 | {:db/ident :community/type, :db/valueType :db.type/ref, :db/cardinality :db.cardinality/many, :db/doc "Community type enum values"}
11 | {:db/ident :community/board :db/valueType :db.type/ref :db/cardinality :db.cardinality/many :db/doc "People who sit on the community board"}
12 | {:db/ident :person/name :db/valueType :db.type/string :db/cardinality :db.cardinality/one :db/doc "A person's full name"}
13 | {:db/ident :neighborhood/name, :db/valueType :db.type/string, :db/cardinality :db.cardinality/one, :db/unique :db.unique/identity, :db/doc "A unique neighborhood name (upsertable)"}
14 | {:db/ident :neighborhood/district, :db/valueType :db.type/ref, :db/cardinality :db.cardinality/one, :db/doc "A neighborhood's district"}
15 | {:db/ident :district/name, :db/valueType :db.type/string, :db/cardinality :db.cardinality/one, :db/unique :db.unique/identity, :db/doc "A unique district name (upsertable)"}
16 | {:db/ident :district/region, :db/valueType :db.type/ref, :db/cardinality :db.cardinality/one, :db/doc "A district region enum value"}]
17 | (index-by :db/ident)))
--------------------------------------------------------------------------------
/src/test/sku_model.clj:
--------------------------------------------------------------------------------
1 | (ns test.sku-model)
2 |
3 | ; https://github.com/Datomic/day-of-datomic/blob/master/tutorial/filters.clj
--------------------------------------------------------------------------------
/src/test/social_news.clj:
--------------------------------------------------------------------------------
1 | (ns test.social-news)
2 |
3 | ; https://gist.github.com/stuarthalloway/2948756
--------------------------------------------------------------------------------
/src/toxiproxy.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | ### INSTALLATION
4 |
5 | # https://github.com/Shopify/toxiproxy
6 | # brew tap shopify/shopify
7 | # brew install toxiproxy
8 |
9 | ### RUNNING
10 |
11 | # ./toxiproxy.sh 8083 8080 200
12 | # toxiproxy-cli list
13 | # killall toxiproxy-server
14 | # toxiproxy-cli toxic help
15 | # toxiproxy-cli toxic update --toxicName hf_latency_toxic --attribute latency=500 hf_dev_proxy
16 |
17 |
18 | log_level=fatal # even 'error' is too spammy
19 |
20 | LOG_LEVEL=${log_level} toxiproxy-server &
21 | echo "waiting for server to come up:"
22 | while ! nc -z localhost 8474; do
23 | printf .
24 | sleep 1
25 | done
26 | echo " server up"
27 | toxiproxy-cli create --listen 0.0.0.0:$1 --upstream localhost:$2 hf_dev_proxy && \
28 | toxiproxy-cli toxic add --toxicName hf_latency_toxic --type latency --attribute latency=$3 hf_dev_proxy
--------------------------------------------------------------------------------
/src/user.clj:
--------------------------------------------------------------------------------
1 | (ns user ; Must be ".clj" file, Clojure doesn't auto-load user.cljc
2 | ;; For fastest REPL startup, no heavy deps here, REPL conveniences only
3 | ;; (Clojure has to compile all this stuff on startup)
4 | (:require [missionary.core :as m]
5 | [hyperfiddle.electric :as e]
6 | [user-main]
7 | hyperfiddle.rcf))
8 |
9 | ; lazy load dev stuff - for faster REPL startup and cleaner dev classpath
10 | (def shadow-start! (delay @(requiring-resolve 'shadow.cljs.devtools.server/start!)))
11 | (def shadow-watch (delay @(requiring-resolve 'shadow.cljs.devtools.api/watch)))
12 | (def shadow-compile (delay @(requiring-resolve 'shadow.cljs.devtools.api/compile)))
13 | (def shadow-release (delay @(requiring-resolve 'shadow.cljs.devtools.api/release)))
14 | (def start-electric-server! (delay (partial @(requiring-resolve 'electric-server-java8-jetty9/start-server!)
15 | (fn [ring-req] (e/boot-server {} user-main/Main ring-req)))))
16 | (def rcf-enable! (delay @(requiring-resolve 'hyperfiddle.rcf/enable!)))
17 |
18 | ; Server-side Electric userland code is lazy loaded by the shadow build.
19 | ; WARNING: make sure your REPL and shadow-cljs are sharing the same JVM!
20 |
21 | (def electric-server-config
22 | {:host "0.0.0.0", :port 8080, :resources-path "public", :manifest-path "public/js/manifest.edn"})
23 |
24 | (defn rcf-shadow-hook {:shadow.build/stages #{:compile-prepare :compile-finish}}
25 | [build-state & args] build-state)
26 |
27 | (defn install-rcf-shadow-hook []
28 | (alter-var-root #'rcf-shadow-hook
29 | (constantly (fn [build-state & args]
30 | ;; NOTE this won’t prevent RCF tests to run during :require-macros phase
31 | (case (:shadow.build/stage build-state)
32 | :compile-prepare (@rcf-enable! false)
33 | :compile-finish (@rcf-enable!))
34 | build-state))))
35 |
36 | (defn main [& args]
37 | (println "Starting Electric compiler and server...")
38 | (@shadow-start!) ; serves index.html as well
39 | (@rcf-enable! false) ; don't run cljs tests on compile (user may have enabled it at the REPL already)
40 | (@shadow-watch :dev) ; depends on shadow server
41 | #_(@shadow-release :dev {:debug false})
42 | ;; todo report clearly if shadow build failed, i.e. due to yarn not being run
43 | (def server (@start-electric-server! electric-server-config))
44 | (comment (.stop server))
45 |
46 | "Datomic Cloud (optional, requires :scratch alias)"
47 | (require '[contrib.datomic-m :as d])
48 | (when (not-empty (eval '(d/detect-datomic-products)))
49 | #_(contrib.datomic-m/install-datomic-onprem)
50 | (eval '(contrib.datomic-m/install-datomic-cloud))
51 | (def datomic-config {:server-type :dev-local :system "datomic-samples"})
52 | ;; install prod globals
53 | (def datomic-client (eval '(d/client datomic-config)))
54 | (def datomic-conn (m/? (eval '(d/connect datomic-client {:db-name "mbrainz-subset"}))))
55 |
56 | ;; install test globals, which are different
57 | (require 'test)
58 | (eval '(test/install-test-state)))
59 |
60 | ;; enable RCF after Datomic is loaded – to resolve circular dependency
61 | (install-rcf-shadow-hook)
62 | (@rcf-enable!))
63 |
64 | ;; shadow-compile vs shadow-release:
65 | ;; https://shadow-cljs.github.io/docs/UsersGuide.html#_development_mode
66 | (defn release "closure optimized target"
67 | [& {:as kwargs}] (@shadow-release :dev (merge {:debug false} kwargs)))
68 |
69 | (when (= "true" (get (System/getenv) "HYPERFIDDLE_AUTO_BOOT"))
70 | (main))
71 |
72 | (comment
73 | (main) ; Electric Clojure(JVM) REPL entrypoint
74 | (hyperfiddle.rcf/enable!) ; turn on RCF after all transitive deps have loaded
75 | (shadow.cljs.devtools.api/repl :dev) ; shadow server hosts the cljs repl
76 | ; connect a second REPL instance to it
77 | ; (DO NOT REUSE JVM REPL it will fail weirdly)
78 | (type 1))
79 |
--------------------------------------------------------------------------------
/src/user.cljs:
--------------------------------------------------------------------------------
1 | (ns user
2 | (:require hyperfiddle.electric
3 | hyperfiddle.rcf
4 | user-main))
5 |
6 | (def electric-main (hyperfiddle.electric/boot-client {} user-main/Main nil))
7 | (defonce reactor nil)
8 |
9 | (defn ^:dev/after-load ^:export start! []
10 | (set! reactor (electric-main
11 | #(js/console.log "Reactor success:" %)
12 | (fn [error]
13 | (case (:hyperfiddle.electric/type (ex-data error))
14 | :hyperfiddle.electric-client/stale-client
15 | (do (js/console.log "Client/server version mismatch, refreshing page.")
16 | (.reload (.-location js/window)))
17 | (js/console.error "Reactor failure:" error)))))
18 | (hyperfiddle.rcf/enable!))
19 |
20 | (defn ^:dev/before-load stop! []
21 | (when reactor (reactor)) ; teardown
22 | (set! reactor nil))
23 |
--------------------------------------------------------------------------------
/src/user.md:
--------------------------------------------------------------------------------
1 | hello world
2 |
3 | ```clojure
4 | (ns prod
5 | (:require hyperfiddle.electric
6 | user-main))
7 |
8 | (def electric-main (hyperfiddle.electric/boot (user-main/Main.)))
9 |
10 | (defn start! []
11 | (electric-main
12 | #(js/console.log "Reactor success:" %)
13 | #(js/console.error "Reactor failure:" %)))
14 | ```
--------------------------------------------------------------------------------
/src/user/demo_10k_dom.cljc:
--------------------------------------------------------------------------------
1 | (ns user.demo-10k-dom
2 | (:require [hyperfiddle.electric :as e]
3 | [hyperfiddle.electric-dom2 :as dom]
4 | [missionary.core :as m]))
5 |
6 | (def !moves #?(:clj (atom []) :cljs nil))
7 | (def !board-size #?(:clj (atom 10000) :cljs nil))
8 | (e/def board-size (e/server (e/watch !board-size)))
9 |
10 | (comment (do (reset! !moves []) (reset! !board-size 2000)))
11 |
12 | #?(:cljs
13 | (defn hot [el]
14 | (m/observe (fn mount [!]
15 | (dom/set-property! el "style" {:background-color "red"})
16 | (! nil) ; initial value
17 | (fn unmount []
18 | (dom/set-property! el "style" {:background-color nil}))))))
19 |
20 | (e/defn Dom-10k-Elements []
21 | (e/client
22 | (dom/h1 (dom/text "10k dom elements (multiplayer)"))
23 | ; fixed width font + inline-block optimizes browser layout
24 | (dom/element "style" (dom/text ".board div { width: 1em; height: 1em; display: inline-block; border: 1px #eee solid; }"))
25 | (dom/element "style" (dom/text ".board { font-family: monospace; font-size: 7px; margin: 0; padding: 0; line-height: 0; }"))
26 | (dom/div {:class "board"}
27 | (e/for [i (range 0 board-size)]
28 | (dom/div
29 | (dom/on "mouseover" (e/fn [e] (e/server (swap! !moves conj i))))))
30 | (e/for [i (e/server (e/watch !moves))]
31 | ; differential side effects, indexed by HTMLCollection
32 | (new (hot (.item (.. dom/node -children) i)))))))
33 |
34 | ;(defn countdown [x] ; Count down to 0 then terminate.
35 | ; (m/relieve {} (m/ap (loop [x x]
36 | ; (m/amb x
37 | ; (if (pos? x)
38 | ; (do (m/? (m/sleep 100))
39 | ; (recur (dec x)))
40 | ; x))))))
41 | ;
42 | ;(defn cell-color [x]
43 | ; (if (> x 1) ; In Electric-land, this conditional would introduce a switch and trigger a ws message for client-server frame coordination.
44 | ; (apply css-rgb-str (hsv->rgb (/ 0 360)
45 | ; (-> x (/ 7.5) (* 1.33))
46 | ; 0.95))
47 | ; "#eee"))
48 | ;
49 | ;#?(:cljs (defn x [!el]
50 | ; (m/observe (fn mount [!]
51 | ; (let [!o (js/MutationObserver !)]
52 | ; (.observe !o !el #js {"attributes" true})
53 | ; (fn unmount [] (.disconnect !o)))))))
--------------------------------------------------------------------------------
/src/user/demo_chat.cljc:
--------------------------------------------------------------------------------
1 | (ns user.demo-chat
2 | (:import [hyperfiddle.electric Pending])
3 | (:require [contrib.data :refer [pad]]
4 | [contrib.str :refer [empty->nil]]
5 | [hyperfiddle.electric :as e]
6 | [hyperfiddle.electric-dom2 :as dom]))
7 |
8 | #?(:clj (defonce !msgs (atom (list))))
9 | (e/def msgs (e/server (pad 10 nil (e/watch !msgs))))
10 |
11 | (e/defn Chat []
12 | (e/client
13 | (try
14 | (dom/ul
15 | (e/server
16 | (e/for-by identity [msg (reverse msgs)] ; chat renders bottom up
17 | (e/client
18 | (dom/li (dom/style {:visibility (if (nil? msg)
19 | "hidden" "visible")})
20 | (dom/text msg))))))
21 |
22 | (dom/input
23 | (dom/props {:placeholder "Type a message" :maxlength 100})
24 | (dom/on "keydown" (e/fn [e]
25 | (when (= "Enter" (.-key e))
26 | (when-some [v (empty->nil (.substr (.. e -target -value) 0 100))]
27 | (e/server (swap! !msgs #(cons v (take 9 %))))
28 | (set! (.-value dom/node) ""))))))
29 | (catch Pending e
30 | (dom/style {:background-color "yellow"})))))
--------------------------------------------------------------------------------
/src/user/demo_chat.md:
--------------------------------------------------------------------------------
1 | What's happening
2 |
3 | * Chat messages are stored in an atom on the server
4 | * It's multiplayer, each connected session sees the same messages
5 | * Messages submit on enter keypress (and clear the input)
6 | * All connected clients see new messages immediately
7 | * The background flashes yellow when something is loading
8 |
9 | Novel forms
10 |
11 | * `Pending`: an exception thrown when the client accesses a remote value that is not yet available
12 | * `try/catch`: reactive try catch
13 | * `dom/input`: a low level DOM control, used here to implement submit-on-enter
14 | * `dom/node`: the live dom node, maintained in dynamic scope for local point writes
15 |
16 | Key Ideas
17 |
18 | * **multiplayer**: the server global state atom is shared by all clients connected to that server, because they share the same JVM.
19 | * **latency**: `e/client` and `e/server` return reactive values across sites. When a remote value is accessed but not yet available, Electric throws a `Pending` exception.
20 | * **reactive try/catch**: in reactive programming, exceptions (like `Pending`) are ephemeral: they can "go away" when an upstream dependency changes, causing the exception to no longer be thrown.
21 | * **JavaScript interop**: cljs interop forms work as expected, side effects are no problem
22 | * **30 LOC**: where is all the client/server framework boilerplate? No GraphQL, no fetch, no API modeling, no async types, etc. One thing missing in this tutorial is error handling (resilient state sync, i.e. optimistic updates with rollback). Electric bundles UI controls for this out of the box, to be discussed in a future tutorial.
23 |
24 | Pending details
25 |
26 | * Q: Why is Pending modeled as an exception? It's not exceptional? A: The semantics match
27 | * Any uncaught Pending exceptions are silenced and disarded by the Electric entrypoint
28 | * When Pending is thrown, the `catch` body is mounted (turning the background yellow)
29 | * Eventually the remote value becomes available and the Pending exception "goes away", unmounting the `catch` body and removing the yellow style.
30 |
31 | Why does each connected client receive realtime updates?
32 |
33 | * each client get's its own "server instance", bound to the websocket session
34 | * `(e/def msgs ...)` is global, therefore shared across all sessions (same JVM)
35 | * other than shared global state, the server instances are independent. All sesison state is isolated and bound to the websocket session, just as HTTP request handlers are bound to a request.
36 |
--------------------------------------------------------------------------------
/src/user/demo_chat_extended.cljc:
--------------------------------------------------------------------------------
1 | (ns user.demo-chat-extended
2 | (:require [contrib.str :refer [empty->nil]]
3 | [hyperfiddle.electric :as e]
4 | [hyperfiddle.electric-dom2 :as dom]))
5 |
6 | #?(:clj (defonce !msgs (atom (list))))
7 | (e/def msgs (e/server (reverse (e/watch !msgs))))
8 |
9 | #?(:clj (defonce !present (atom {}))) ; session-id -> user
10 | (e/def present (e/server (e/watch !present)))
11 |
12 | (e/defn Chat-UI [username]
13 | (e/client
14 | (dom/div (dom/text "Present: "))
15 | (dom/ul
16 | (e/server
17 | (e/for-by first [[session-id username] present]
18 | (e/client
19 | (dom/li (dom/text username (str " (session-id: " session-id ")")))))))
20 |
21 | (dom/hr)
22 | (dom/ul
23 | (e/server
24 | (e/for [{:keys [::username ::msg]} msgs]
25 | (e/client
26 | (dom/li (dom/strong (dom/text username))
27 | (dom/text " " msg))))))
28 |
29 | (dom/input
30 | (dom/props {:placeholder "Type a message" :maxlength 100})
31 | (dom/on "keydown" (e/fn [e]
32 | (when (= "Enter" (.-key e))
33 | (when-some [v (empty->nil (.substr (.. e -target -value) 0 100))]
34 | (dom/style {:background-color "yellow"}) ; loading
35 | (e/server
36 | (swap! !msgs #(cons {::username username ::msg v}
37 | (take 9 %))))
38 | (set! (.-value dom/node) ""))))))))
39 |
40 | (e/defn ChatExtended []
41 | (e/client
42 | (let [session-id
43 | (e/server (get-in e/http-request [:headers "sec-websocket-key"]))
44 | username
45 | (e/server (get-in e/http-request [:cookies "username" :value]))]
46 | (if-not (some? username)
47 | (dom/div
48 | (dom/text "Set login cookie here: ")
49 | (dom/a (dom/props {::dom/href "/auth"}) (dom/text "/auth"))
50 | (dom/text " (blank password)"))
51 | (do
52 | (e/server
53 | (swap! !present assoc session-id username)
54 | (e/on-unmount #(swap! !present dissoc session-id)))
55 | (dom/div (dom/text "Authenticated as: " username))
56 | (Chat-UI. username))))))
57 |
--------------------------------------------------------------------------------
/src/user/demo_chat_extended.md:
--------------------------------------------------------------------------------
1 | What's happening
2 | * There is auth
3 | * Presence – logged in users can see who else is in the room (try two tabs)
4 |
5 | Novel forms
6 | * `e/*http-request*` the ring request that established the websocket connection
7 | * `do` sequences effects, returning the final result (same as Clojure). Reactive objects in the body are constructed in order and then run concurrently, so e.g. `(do (Blinker.) (Blinker.))` will have concurrent blinkers.
8 |
9 | Key ideas
10 | * **regular web server**: Electric's websocket can be hosted by any ordinary Clojure web server, you provide this.
11 | * **auth** is same as any other web app, you have access to the request, cookies, etc
12 |
13 | HTTP server details
14 | * App server for this app: [`electric_server_java8_jetty9.clj`](https://github.com/hyperfiddle/electric-examples-app/blob/main/src/electric_server_java8_jetty9.clj)
15 | * it hosts a websocket
16 | * it has a `/auth` endpoint configured with HTTP Basic Auth
17 | * Note the server is application code, it's not provided by the Electric library dependency.
18 | * Q: Why is it not included in the library? A: because it has hardcoded http routes and auth examples
19 | * Instead we provide [templates](https://github.com/hyperfiddle/electric-starter-app/tree/main/src) in the starter app for you to modify.
20 |
21 | More on Electric functions/objects and dynamic extent
22 | * "Extent" is defined in the [previous tutorial](/user.tutorial-lifecycle!Lifecycle)
23 | * Here, `dom/on`'s callback, the `e/fn`, is an *object* and has *extent*
24 | * Using the railroad switch metaphor, the extent of the object/node is the time during which the callback is active/alive/running.
25 | * `dom/on` is managing the lifetime/extent of the callback `e/fn` by observing `Pending` exceptions it throws.
26 | * `e/server` throws `Pending` until the result of the body is known to the client (did it succeed? did it throw?)
27 | * When the `Pending` exception goes away, `dom/on` will unmount the `e/fn` and remove it from the DAG.
28 | * This means you can put resources (e.g. allocate DOM) inside the callback, as we do here with `(dom/style {:background-color "yellow"})`.
29 |
30 | When you send a message and the server effect is pending, now the input flashes yellow instead of the whole page
31 | * The style is mounted when it's parent e/fn is mounted, and the style is unmounted when the e/fn is unmounted.
32 | * So the style's extent is the duration of the callback, and the callback is the duration of the Pending exception plus one tick for the final result. Once the result is known, the callback (and the style) are unmounted and removed from the DAG.
--------------------------------------------------------------------------------
/src/user/demo_color.cljc:
--------------------------------------------------------------------------------
1 | (ns user.demo-color
2 | (:require [contrib.data :refer [assoc-vec]]
3 | [hyperfiddle.electric :as e]
4 | [hyperfiddle.electric-dom2 :as dom]
5 | [hyperfiddle.history :as router]
6 | [contrib.color :as c]))
7 |
8 | ;; Goal is to demonstrate:
9 | ;; - fine-grained reactivity on CSS properties
10 | ;; - Non-trivial DOM api usage (canvas)
11 |
12 | (def CANVAS-WIDTH 360) ; px
13 | (def CANVAS-HEIGHT 100) ; px
14 |
15 | (defn format-rgb [[r g b]] (str "rgb("r","g","b")"))
16 |
17 | #?(:cljs
18 | (defn draw! [^js canvas colorf]
19 | (let [ctx (.getContext canvas "2d")]
20 | (loop [angle 0]
21 | (set! (.-strokeStyle ctx) (colorf angle))
22 | (.beginPath ctx)
23 | (.moveTo ctx angle 0)
24 | (.lineTo ctx angle CANVAS-HEIGHT)
25 | (.closePath ctx)
26 | (.stroke ctx)
27 | (when (< angle 360)
28 | (recur (inc angle)))))))
29 |
30 | #?(:cljs
31 | (defn draw-gradient! [canvas hue colorf]
32 | (draw! canvas (fn [angle] (format-rgb (if (= angle hue) [255 255 255] (colorf angle)))))))
33 |
34 | (defn saturation->chroma [saturation] (* 0.158 (/ saturation 100)))
35 |
36 | (e/defn Tile [color]
37 | (e/client
38 | (dom/div (dom/props {:style {:display :flex
39 | :align-items :center
40 | :justify-content :center
41 | :color :white
42 | :background-color (format-rgb color)
43 | :width "100px"
44 | :height "100%"
45 | }})
46 | (dom/text "Contrast"))))
47 |
48 | (e/defn Color []
49 | (e/client
50 | (let [[self h s l] router/route
51 | h (or h 180)
52 | s (or s 80)
53 | l (or l 70)
54 | swap-route! router/swap-route!]
55 | (dom/div (dom/props {:style {:display :grid
56 | :grid-template-columns "auto 1fr auto"
57 | :gap "0 1rem"
58 | :align-items :center
59 | :justify-items :stretch
60 | :max-width "600px"}})
61 | (dom/p (dom/text "Lightness"))
62 | (dom/input (dom/props {:type :range
63 | :min 0
64 | :max 100
65 | :step 1
66 | :value l})
67 | (dom/on! "input" (fn [^js e] (swap-route! assoc-vec 3 (js/parseInt (.. e -target -value))))))
68 | (dom/p (dom/text l "%"))
69 |
70 | (dom/p (dom/text "Saturation"))
71 | (dom/input (dom/props {:type :range
72 | :min 0
73 | :max 100
74 | :step 1
75 | :value s})
76 | (dom/on! "input" (fn [^js e] (swap-route! assoc-vec 2 (js/parseInt (.. e -target -value))))))
77 | (dom/p (dom/text s "%"))
78 |
79 |
80 | (dom/p (dom/text "Hue"))
81 | (dom/input (dom/props {:type :range
82 | :min 0
83 | :max 360
84 | :step 1
85 | :value h})
86 | (dom/on! "input" (fn [^js e] (swap-route! assoc-vec 1 (js/parseInt (.. e -target -value))))))
87 | (dom/p (dom/text h "°"))
88 |
89 |
90 | (dom/p (dom/text "HSL"))
91 | (dom/canvas (dom/props {:width 360
92 | :height 100})
93 | (draw-gradient! dom/node h (fn [h] (c/hsl->rgb [h s l])))
94 | )
95 | (Tile. (c/hsl->rgb [h s l]))
96 |
97 | (dom/p (dom/text "OKLCH"))
98 | (dom/canvas (dom/props {:width 360
99 | :height 100})
100 | (draw-gradient! dom/node h (fn [h] (c/oklch->rgb [l (saturation->chroma s) h]))))
101 | (Tile. (c/oklch->rgb [l (saturation->chroma s) h]))
102 |
103 | (dom/p (dom/text "HSLuv"))
104 | (dom/canvas (dom/props {:width 360
105 | :height 100})
106 | (draw-gradient! dom/node h (fn [h] (c/hsluv->rgb [h s l]))))
107 | (Tile. (c/hsluv->rgb [h s l]))))))
108 |
--------------------------------------------------------------------------------
/src/user/demo_explorer.cljc:
--------------------------------------------------------------------------------
1 | (ns user.demo-explorer
2 | (:require [clojure.datafy :refer [datafy]]
3 | [clojure.core.protocols :refer [nav]]
4 | #?(:clj clojure.java.io)
5 | [clojure.string :as str]
6 | [contrib.data :refer [treelister]]
7 | [contrib.datafy-fs #?(:clj :as :cljs :as-alias) fs]
8 | [hyperfiddle.electric :as e]
9 | [hyperfiddle.electric-dom2 :as dom]
10 | [hyperfiddle.history :as router]
11 | [contrib.gridsheet :as gridsheet :refer [Explorer]]))
12 |
13 | (def unicode-folder "\uD83D\uDCC2") ; 📂
14 |
15 | (e/defn Render-cell [m a]
16 | (let [v (a m)]
17 | (case a
18 | ::fs/name (case (::fs/kind m)
19 | ::fs/dir (let [absolute-path (::fs/absolute-path m)]
20 | (e/client (router/link absolute-path (dom/text v))))
21 | (::fs/other ::fs/symlink ::fs/unknown-kind) (e/client (dom/text v))
22 | (e/client (dom/text v)))
23 | ::fs/modified (e/client (some-> v .toLocaleDateString dom/text))
24 | ::fs/kind (case (::fs/kind m)
25 | ::fs/dir (e/client (dom/text unicode-folder))
26 | (e/client (some-> v name dom/text)))
27 | (e/client (dom/text (str v))))))
28 |
29 | (e/defn Dir [x]
30 | (let [m (datafy x)
31 | xs (nav m ::fs/children (::fs/children m))]
32 | (e/client (dom/div (dom/text (e/server (::fs/absolute-path m)))))
33 | (Explorer.
34 | (treelister xs ::fs/children #(str/includes? (str/lower-case (::fs/name %))
35 | (str/lower-case (str %2))))
36 | {::dom/style {:height "calc((20 + 1) * 24px)"}
37 | ::gridsheet/page-size 20
38 | ::gridsheet/row-height 24
39 | ::gridsheet/Format Render-cell
40 | ::gridsheet/columns [::fs/name ::fs/modified ::fs/size ::fs/kind]
41 | ::gridsheet/grid-template-columns "auto 8em 5em 3em"})))
42 |
43 | (e/defn DirectoryExplorer []
44 | (e/client
45 | (dom/link (dom/props {:rel :stylesheet, :href "/user/gridsheet-optional.css"}))
46 | (dom/div (dom/props {:class "user-gridsheet-demo"})
47 | (binding [router/build-route (fn [[self state] path]
48 | ; root local links through this entrypoint
49 | [self (assoc state ::path path)])]
50 | (e/server
51 | (let [{:keys [::path]} (e/client router/route)
52 | fs-path (or path (fs/absolute-path "./"))]
53 | (Dir. (clojure.java.io/file fs-path))))))))
54 |
55 | ; Improvements
56 | ; Native search
57 | ; lazy folding/unfolding directories (no need for pagination)
58 | ; forms (currently table hardcoded with recursive pull)
59 |
--------------------------------------------------------------------------------
/src/user/demo_explorer.md:
--------------------------------------------------------------------------------
1 | What's happening
2 | * Thousands of elements efficiently streamed from server to client
3 | * virtual pagination (the same 20 divs are reused — check the DOM and confirm)
4 | * `e/for` is transparently coordinating the network (same as previous [System Properties demo](/user.demo-system-properties!SystemProperties))
5 |
6 | Key ideas
7 |
8 | * **performance**: `e/for` is awesome, look how fast it is
9 | * **diffing**: `e/for` performs diffing to reuse child state (DOM elements) and streams only deltas (mount/unmount/update/move).
10 | * **network-transparent abstraction**: This demo is [50 LOC](https://github.com/hyperfiddle/electric-examples-app/blob/main/src/user/demo_explorer.cljc) (it also uses a gridsheet component which is [92 LOC](https://github.com/hyperfiddle/electric/blob/master/src/contrib/gridsheet.cljc), so 142 LOC total)
11 | * **compiler managed network**: How would you do this in React?
12 |
13 |
14 | Network-transparent abstraction is the whole point of Electric Clojure, see (a more filled out version of basically this same demo) for more content about network-transparent abstraction.
15 |
16 | For more detail about reactive network efficiency, see [this clojureverse answer](https://clojureverse.org/t/electric-clojure-a-signals-dsl-for-fullstack-web-ui/9788/32?u=dustingetz).
17 |
--------------------------------------------------------------------------------
/src/user/demo_index.cljc:
--------------------------------------------------------------------------------
1 | (ns user.demo-index
2 | (:require [hyperfiddle.electric :as e]
3 | [hyperfiddle.electric-dom2 :as dom]
4 | [hyperfiddle.history :as router])) ; for link only
5 |
6 | (def pages
7 | [`user.demo-two-clocks/TwoClocks
8 | `user.demo-toggle/Toggle
9 | `user.demo-system-properties/SystemProperties
10 | `user.demo-webview/Webview
11 | `user.demo-chat/Chat
12 | `user.tutorial-lifecycle/Lifecycle
13 | `user.demo-chat-extended/ChatExtended
14 | `user.demo-todos-simple/TodoList
15 | `user.demo-reagent-interop/ReagentInterop
16 | `user.demo-svg/SVG])
17 |
18 | (def seven-guis
19 | [`user.tutorial-7guis-1-counter/Counter
20 | `user.tutorial-7guis-2-temperature/TemperatureConverter
21 | `user.tutorial-7guis-4-timer/Timer
22 | `user.tutorial-7guis-5-crud/CRUD])
23 |
24 | (def demos
25 | [`user.demo-explorer/DirectoryExplorer
26 | `user.demo-todomvc/TodoMVC
27 | `user.demo-todomvc-composed/TodoMVC-composed
28 | `user.demo-color/Color])
29 |
30 | (def secret-pages
31 | [`wip.teeshirt-orders/Webview-HFQL
32 | `wip.demo-explorer2/DirectoryExplorer-HFQL
33 | ;`user.demo-10k-dom/Dom-10k-Elements
34 | `wip.demo-branched-route/RecursiveRouter
35 | `wip.tag-picker/TagPicker
36 | `wip.demo-custom-types/CustomTypes
37 | `wip.tracing/TracingDemo
38 | ;`user.demo-tic-tac-toe/TicTacToe
39 | ;`user.demo-virtual-scroll/VirtualScroll
40 |
41 | ; need extra deps alias
42 | ;::dennis-exception-leak
43 | #_`wip.demo-stage-ui4/CrudForm
44 | #_`wip.datomic-browser/DatomicBrowser])
45 |
46 | (e/defn Demos []
47 | (e/client
48 | (dom/h3 (dom/text "Tutorial"))
49 | (e/for [k pages]
50 | (dom/div (router/link [k] (dom/text (name k)))))
51 |
52 | (dom/h3 (dom/text "7 GUIs"))
53 | (e/for [k seven-guis]
54 | (dom/div (router/link [k] (dom/text (name k)))))
55 |
56 | (dom/h3 (dom/text "Demos")) ; no source code here, can link to GH
57 | (e/for [k demos]
58 | (dom/div (router/link [k] (dom/text (name k)))))
59 |
60 | (dom/div (dom/style {:opacity 0})
61 | (router/link [`Secrets] (dom/text "secret-hyperfiddle-demos")))))
62 |
63 | (e/defn Secrets []
64 | (e/client
65 | (dom/h1 (dom/text "Wip unpublished demos (unstable/wip)")
66 | (dom/comment_ "ssh" "it's a secret"))
67 | (dom/p "Some require a database connection and are often broken.")
68 | (e/for [k secret-pages]
69 | (dom/div (router/link [k] (dom/text (name k)))))))
70 |
--------------------------------------------------------------------------------
/src/user/demo_reagent_interop.cljc:
--------------------------------------------------------------------------------
1 | (ns user.demo-reagent-interop
2 | #?(:cljs (:require-macros [user.demo-reagent-interop :refer [with-reagent]]))
3 | (:require [hyperfiddle.electric :as e]
4 | [hyperfiddle.electric-dom2 :as dom]
5 | #?(:cljs [reagent.core :as r])
6 | #?(:cljs ["recharts" :refer [ScatterChart Scatter LineChart Line
7 | XAxis YAxis CartesianGrid]])
8 | #?(:cljs ["react-dom/client" :as ReactDom])))
9 |
10 | #?(:cljs (def ReactRootWrapper
11 | (r/create-class
12 | {:component-did-mount (fn [this] (js/console.log "mounted"))
13 | :render (fn [this]
14 | (let [[_ Component & args] (r/argv this)]
15 | (into [Component] args)))})))
16 |
17 | #?(:cljs (defn create-root
18 | "See https://reactjs.org/docs/react-dom-client.html#createroot"
19 | ([node] (create-root node (str (gensym))))
20 | ([node id-prefix]
21 | (ReactDom/createRoot node #js {:identifierPrefix id-prefix}))))
22 |
23 | #?(:cljs (defn render [root & args]
24 | (.render root (r/as-element (into [ReactRootWrapper] args)))))
25 |
26 | (defmacro with-reagent [& args]
27 | `(dom/div ; React will hijack this element and empty it.
28 | (let [root# (create-root dom/node)]
29 | (render root# ~@args)
30 | (e/on-unmount #(.unmount root#)))))
31 |
32 | ;; Reagent World
33 |
34 | (defn TinyLineChart [data]
35 | #?(:cljs
36 | [:> LineChart {:width 400 :height 200 :data (clj->js data)}
37 | [:> CartesianGrid {:strokeDasharray "3 3"}]
38 | [:> XAxis {:dataKey "name"}]
39 | [:> YAxis]
40 | [:> Line {:type "monotone", :dataKey "pv", :stroke "#8884d8"}]
41 | [:> Line {:type "monotone", :dataKey "uv", :stroke "#82ca9d"}]]))
42 |
43 | (defn MousePosition [x y]
44 | #?(:cljs
45 | [:> ScatterChart {:width 300 :height 300
46 | :margin #js{:top 20, :right 20, :bottom 20, :left 20}}
47 | [:> CartesianGrid {:strokeDasharray "3 3"}]
48 | [:> XAxis {:type "number", :dataKey "x", :unit "px", :domain #js[0 2000]}]
49 | [:> YAxis {:type "number", :dataKey "y", :unit "px", :domain #js[0 2000]}]
50 | [:> Scatter {:name "Mouse position",
51 | :data (clj->js [{:x x, :y y}]), :fill "#8884d8"}]]))
52 |
53 | ;; Electric Clojure
54 |
55 | (e/defn ReagentInterop []
56 | (e/client
57 | (let [[x y] (dom/on! js/document "mousemove"
58 | (fn [e] [(.-clientX e) (.-clientY e)]))]
59 | (with-reagent MousePosition x y) ; reactive
60 | ;; Adapted from https://recharts.org/en-US/examples/TinyLineChart
61 | (with-reagent TinyLineChart
62 | [{:name "Page A" :uv 4000 :amt 2400 :pv 2400}
63 | {:name "Page B" :uv 3000 :amt 2210 :pv 1398}
64 | {:name "Page C" :uv 2000 :amt 2290 :pv (+ 6000 (* -5 y))} ; reactive
65 | {:name "Page D" :uv 2780 :amt 2000 :pv 3908}
66 | {:name "Page E" :uv 1890 :amt 2181 :pv 4800}
67 | {:name "Page F" :uv 2390 :amt 2500 :pv 3800}
68 | {:name "Page G" :uv 3490 :amt 2100 :pv 4300}]))))
--------------------------------------------------------------------------------
/src/user/demo_svg.cljc:
--------------------------------------------------------------------------------
1 | (ns user.demo-svg
2 | (:require [hyperfiddle.electric :as e]
3 | [hyperfiddle.electric-dom2 :as dom]
4 | [hyperfiddle.electric-svg :as svg]))
5 |
6 | (defn wave [time]
7 | (Math/cos (/ (* (mod (Math/round (/ time 10)) 360) Math/PI) 180)))
8 |
9 | (e/defn SVG []
10 | (e/client
11 | (let [offset (* 3 (wave e/system-time-ms))] ; js animation clock
12 | (svg/svg (dom/props {:viewBox "0 0 300 100"})
13 | (svg/circle
14 | (dom/props {:cx 50 :cy 50 :r (+ 30 offset)
15 | :style {:fill "#af7ac5 "}}))
16 | (svg/g
17 | (dom/props {:transform
18 | (str "translate(105,20) rotate(" (* 3 offset) ")")})
19 | (svg/polygon (dom/props {:points "30,0 0,60 60,60"
20 | :style {:fill "#5499c7"}})))
21 | (svg/rect
22 | (dom/props {:x 200 :y 20 :width (+ 60 offset) :height (+ 60 offset)
23 | :style {:fill "#45b39d"}}))))))
--------------------------------------------------------------------------------
/src/user/demo_svg.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hyperfiddle/electric-v2-tutorial/fb5df89804ecc2d02916573897bb7ecca23940fe/src/user/demo_svg.md
--------------------------------------------------------------------------------
/src/user/demo_system_properties.cljc:
--------------------------------------------------------------------------------
1 | (ns user.demo-system-properties
2 | (:require [clojure.string :as str]
3 | [hyperfiddle.electric :as e]
4 | [hyperfiddle.electric-dom2 :as dom]
5 | [hyperfiddle.electric-ui4 :as ui]))
6 |
7 | #?(:clj
8 | (defn jvm-system-properties [?s]
9 | (->> (System/getProperties)
10 | (filter (fn [^java.util.concurrent.ConcurrentHashMap$MapEntry kv]
11 | (str/includes? (str/lower-case (str (key kv)))
12 | (str/lower-case (str ?s)))))
13 | (sort-by key))))
14 |
15 | (e/defn SystemProperties []
16 | (e/client
17 | (let [!search (atom "")
18 | search (e/watch !search)]
19 | (e/server
20 | (let [system-props (jvm-system-properties search)
21 | matched-count (count system-props)]
22 | (e/client
23 | (dom/div (dom/text matched-count " matches"))
24 | (ui/input search (e/fn [v] (reset! !search v))
25 | (dom/props {:type "search" :placeholder "🔎 java.class.path"}))
26 | (dom/table
27 | (dom/tbody
28 | (e/server
29 | ; reactive for, stabilized with "react key"
30 | (e/for-by key [[k v] system-props]
31 | (e/client
32 | (println 'rendering k)
33 | (dom/tr
34 | (dom/td (dom/text k))
35 | (dom/td (dom/text v))))))))))))))
--------------------------------------------------------------------------------
/src/user/demo_system_properties.md:
--------------------------------------------------------------------------------
1 | What's happening
2 |
3 | * There's a HTML table on the frontend, backed by a backend "query" `jvm-system-properties`
4 | * The backend query is an ordinary Clojure function that only exists on the server.
5 | * Typing into the frontend input causes the backend query to rerun and update the table.
6 | * There's a reactive for loop to render the table.
7 | * The view code deeply nests client and server calls, arbitrarily, even through loops.
8 |
9 | Novel forms
10 |
11 | * `ui/input`: a text input control with "batteries included" loading/syncing state.
12 | * `e/for-by`: a reactive map operator, stabilized to bind each child branch state (e.g. DOM element) to an entity in the collection by id (provided by userland fn - similar to [React.js key](https://stackoverflow.com/questions/28329382/understanding-unique-keys-for-array-children-in-react-js/43892905#43892905)).
13 |
14 | Key ideas
15 |
16 | * **ordinary Clojure/Script functions**: `clojure.core/defn` works as it does in Clojure/Script, it's still a normal blocking function and is opaque to Electric. Electric does not mess with the `clojure.core/defn` macro.
17 | * **query can be any function**: return collections, SQL resultsets, whatever
18 | * **direct query/view composition**: `jvm-system-properties`, a server function, composes directly with the frontend DOM table. Thus unifying your code into one paradigm, promoting readability, and making it easier to craft complex interactions between client and server components, maintain and refactor them.
19 | * **reactive-for**: The table rows are renderered by a for loop. Reactive loops are efficient and recompute branches only precisely when needed.
20 | * **network transfer can be reasoned about clearly**: values are only transferred between sites when and if they are used. The `system-props` collection is never actually accessed from a client region and therefore never escapes the server.
21 |
22 | Reactive for details
23 |
24 | * `e/for-by` ensures that each table row is bound to a logical element of the collection, and only touched when a row dependency changes.
25 | * Notice there is a `println` inside the for loop. This is so you can see the efficient rendering in the browser console.
26 | * Open the browser console now and confirm for yourself:
27 | * On initial render, each row is rendered once
28 | * Slowly input "java.class.path"
29 | * As you narrow the filter, no rows are recomputed. (The existing dom is reused, so there is nothing to recompute because neither `k` nor `v` have changed for that row.)
30 | * Slowly backspace, one char at a time
31 | * As you widen the filter, rows are computed as they come back. That's because they were unmounted and discarded!
32 | * Quiz: Try setting an inline style "background-color: red" on element "java.class.path". When is the style retained? When is the style lost? Why?
33 |
34 | Reasoning about network transfer
35 |
36 | * Look at which remote scope values are closed over and accessed.
37 | * Only remote access is transferred. Mere *availability* in scope does not transfer.
38 | * In the `e/for-by`, `k` and `v` exist in a server scope, and yet are accessed from a client scope.
39 | * Electric tracks this and sends a stream of individual `k` and `v` updates over network.
40 | * The collection value `system-props` is not accessed from client scope, so Electric will not move it. Values are only moved if they are accessed.
41 |
42 | Network transparent composition is not the heavy, leaky abstraction you might think it is
43 |
44 | * The DAG representation of the program makes this simple to do
45 | * The electric core implementation is about 3000 LOC
46 | * Function composition laws are followed, Electric functions are truly functions.
47 | * Functions are an abstract mathematical object
48 | * Javascript already generalizes from function -> async function (`async/await`) -> generator function (`fn*/yield`)
49 | * Electric generalizes further: stream function -> reactive function -> distributed function
50 | * With Electric, you can refactor across the frontend/backend boundary, all in one place, without caring about any plumbing.
51 | * Refactoring is an algebraic activity with local reasoning, just as it should be.
52 | * Functional programming without the BS
53 |
--------------------------------------------------------------------------------
/src/user/demo_tic_tac_toe.cljc:
--------------------------------------------------------------------------------
1 | (ns user.demo-tic-tac-toe
2 | (:require [hyperfiddle.electric :as e]
3 | [hyperfiddle.electric-dom2 :as dom]
4 | [hyperfiddle.electric-ui4 :as ui]))
5 |
6 | (def !x #?(:clj (atom (vec (repeat 10 0))) :cljs nil))
7 | (e/def x (e/server (e/watch !x)))
8 | (defn update-board [board pos] (update board pos #(case % 0 1, 1 2, 2 0)))
9 |
10 | (e/defn Button [offset]
11 | (e/client
12 | (ui/button (e/fn []
13 | (e/server
14 | (swap! !x update-board offset)))
15 | (dom/text
16 | (case (e/server (nth x offset))
17 | 2 "x"
18 | 1 "o"
19 | 0 "-")))))
20 |
21 | (e/defn TicTacToe []
22 | (e/client
23 | (dom/h1 (dom/text "Tic Tac Toe \uD83C\uDFAE"))
24 | (dom/p (dom/text "multiplayer works, try two tabs"))
25 | (dom/table
26 | (e/for [row [[0 1 2]
27 | [3 4 5]
28 | [6 7 8]]]
29 | (dom/tr
30 | (e/for [i row]
31 | (dom/td
32 | (Button. i))))))))
33 |
--------------------------------------------------------------------------------
/src/user/demo_todomvc.cljc:
--------------------------------------------------------------------------------
1 | (ns user.demo-todomvc
2 | "Requires -Xss2m to compile. The Electric compiler exceeds the default 1m JVM ThreadStackSize
3 | due to large macroexpansion resulting in false StackOverflowError during analysis."
4 | (:require
5 | contrib.str
6 | #?(:clj [datascript.core :as d])
7 | [hyperfiddle.electric :as e]
8 | [hyperfiddle.electric-dom2 :as dom]
9 | [hyperfiddle.electric-ui4 :as ui]))
10 |
11 | #?(:clj (defonce !conn (d/create-conn {}))) ; server
12 | (e/def db) ; server
13 | (e/def transact!) ; server
14 | #?(:cljs (def !state (atom {::filter :all ; client
15 | ::editing nil
16 | ::delay 0})))
17 |
18 | #?(:clj
19 | (defn query-todos [db filter]
20 | {:pre [filter]}
21 | (case filter
22 | :active (d/q '[:find [?e ...] :where [?e :task/status :active]] db)
23 | :done (d/q '[:find [?e ...] :where [?e :task/status :done]] db)
24 | :all (d/q '[:find [?e ...] :where [?e :task/status]] db))))
25 |
26 | #?(:clj
27 | (defn todo-count [db filter]
28 | {:pre [filter]
29 | :post [(number? %)]}
30 | (-> (case filter
31 | :active (d/q '[:find (count ?e) . :where [?e :task/status :active]] db)
32 | :done (d/q '[:find (count ?e) . :where [?e :task/status :done]] db)
33 | :all (d/q '[:find (count ?e) . :where [?e :task/status]] db))
34 | (or 0)))) ; datascript can return nil wtf
35 |
36 | (e/defn Filter-control [state target label]
37 | (e/client
38 | (dom/a (dom/props {:class (when (= state target) "selected")})
39 | (dom/text label)
40 | (dom/on "click" (e/fn [_] (swap! !state assoc ::filter target))))))
41 |
42 |
43 | (e/defn TodoStats [state]
44 | (e/client
45 | (let [active (e/server (todo-count db :active))
46 | done (e/server (todo-count db :done))]
47 | (dom/div
48 | (dom/span (dom/props {:class "todo-count"})
49 | (dom/strong (dom/text active))
50 | (dom/span (dom/text " " (str (case active 1 "item" "items")) " left")))
51 |
52 | (dom/ul (dom/props {:class "filters"})
53 | (dom/li (Filter-control. (::filter state) :all "All"))
54 | (dom/li (Filter-control. (::filter state) :active "Active"))
55 | (dom/li (Filter-control. (::filter state) :done "Completed")))
56 |
57 | (when (pos? done)
58 | (ui/button (e/fn [] (e/server (when-some [ids (seq (query-todos db :done))]
59 | (transact! (mapv (fn [id] [:db/retractEntity id]) ids)) nil)))
60 | (dom/props {:class "clear-completed"})
61 | (dom/text "Clear completed " done)))))))
62 |
63 | (e/defn TodoItem [state id]
64 | (e/server
65 | (let [{:keys [:task/status :task/description]} (d/entity db id)]
66 | (e/client
67 | (dom/li
68 | (dom/props {:class [(when (= :done status) "completed")
69 | (when (= id (::editing state)) "editing")]})
70 | (dom/div (dom/props {:class "view"})
71 | (ui/checkbox (= :done status) (e/fn [v]
72 | (let [status (case v true :done, false :active, nil)]
73 | (e/server (transact! [{:db/id id, :task/status status}]) nil)))
74 | (dom/props {:class "toggle"}))
75 | (dom/label (dom/text description)
76 | (dom/on "dblclick" (e/fn [_] (swap! !state assoc ::editing id)))))
77 | (when (= id (::editing state))
78 | (dom/span (dom/props {:class "input-load-mask"})
79 | (dom/on-pending (dom/props {:aria-busy true})
80 | (dom/input
81 | (dom/on "keydown"
82 | (e/fn [e]
83 | (case (.-key e)
84 | "Enter" (when-some [description (contrib.str/blank->nil (-> e .-target .-value))]
85 | (case (e/server (transact! [{:db/id id, :task/description description}]) nil)
86 | (swap! !state assoc ::editing nil)))
87 | "Escape" (swap! !state assoc ::editing nil)
88 | nil)))
89 | (dom/props {:class "edit" #_#_:autofocus true})
90 | (dom/bind-value description) ; first set the initial value, then focus
91 | (case description ; HACK sequence - run focus after description is available
92 | (.focus dom/node))))))
93 | (ui/button (e/fn [] (e/server (transact! [[:db/retractEntity id]]) nil))
94 | (dom/props {:class "destroy"})))))))
95 |
96 | #?(:clj
97 | (defn toggle-all! [db status]
98 | (let [ids (query-todos db (if (= :done status) :active :done))]
99 | (map (fn [id] {:db/id id, :task/status status}) ids))))
100 |
101 | (e/defn TodoList [state]
102 | (e/client
103 | (dom/div
104 | (dom/section (dom/props {:class "main"})
105 | (let [active (e/server (todo-count db :active))
106 | all (e/server (todo-count db :all))
107 | done (e/server (todo-count db :done))]
108 | (ui/checkbox (cond (= all done) true
109 | (= all active) false
110 | :else nil)
111 | (e/fn [v] (let [status (case v (true nil) :done, false :active)]
112 | (e/server (transact! (toggle-all! db status)) nil)))
113 | (dom/props {:class "toggle-all"})))
114 | (dom/label (dom/props {:for "toggle-all"}) (dom/text "Mark all as complete"))
115 | (dom/ul (dom/props {:class "todo-list"})
116 | (e/for [id (e/server (sort (query-todos db (::filter state))))]
117 | (TodoItem. state id)))))))
118 |
119 | (e/defn CreateTodo []
120 | (e/client
121 | (dom/span (dom/props {:class "input-load-mask"})
122 | (dom/on-pending (dom/props {:aria-busy true})
123 | (dom/input
124 | (dom/on "keydown"
125 | (e/fn [e]
126 | (when (= "Enter" (.-key e))
127 | (when-some [description (contrib.str/empty->nil (-> e .-target .-value))]
128 | (e/server (transact! [{:task/description description, :task/status :active}]) nil)
129 | (set! (.-value dom/node) "")))))
130 | (dom/props {:class "new-todo", :placeholder "What needs to be done?"}))))))
131 |
132 | (e/defn TodoMVC-UI [state]
133 | (e/client
134 | (dom/section (dom/props {:class "todoapp"})
135 | (dom/header (dom/props {:class "header"})
136 | (CreateTodo.))
137 | (when (e/server (pos? (todo-count db :all)))
138 | (TodoList. state))
139 | (dom/footer (dom/props {:class "footer"})
140 | (TodoStats. state)))))
141 |
142 | (e/defn TodoMVC-body [state]
143 | (e/client
144 | (dom/div (dom/props {:class "todomvc"})
145 | (TodoMVC-UI. state)
146 | (dom/footer (dom/props {:class "info"})
147 | (dom/p (dom/text "Double-click to edit a todo"))))))
148 |
149 | (e/defn Diagnostics [state]
150 | (e/client
151 | (dom/h1 (dom/text "Diagnostics"))
152 | (dom/dl
153 | (dom/dt (dom/text "count :all")) (dom/dd (dom/text (pr-str (e/server (todo-count db :all)))))
154 | (dom/dt (dom/text "query :all")) (dom/dd (dom/text (pr-str (e/server (query-todos db :all)))))
155 | (dom/dt (dom/text "state")) (dom/dd (dom/text (pr-str state)))
156 | (dom/dt (dom/text "delay")) (dom/dd
157 | (ui/long (::delay state) (e/fn [v] (swap! !state assoc ::delay v))
158 | (dom/props {:step 1, :min 0, :style {:width :min-content}}))
159 | (dom/text " ms")))))
160 |
161 | #?(:clj
162 | (defn slow-transact! [!conn delay tx]
163 | (try (Thread/sleep delay) ; artificial latency
164 | (d/transact! !conn tx)
165 | (catch InterruptedException _))))
166 |
167 | (e/defn TodoMVC []
168 | (e/client
169 | (let [state (e/watch !state)]
170 | (e/server
171 | (binding [db (e/watch !conn)
172 | transact! (partial slow-transact! !conn (e/client (::delay state)))]
173 | (e/client
174 | (dom/link (dom/props {:rel :stylesheet, :href "/todomvc.css"}))
175 | ; exclude #root style from todomvc-composed by inlining here
176 | (TodoMVC-body. state)
177 | #_(Diagnostics. state)))))))
178 |
179 | (comment
180 | (todo-count @!conn :all)
181 | (todo-count @!conn :active)
182 | (todo-count @!conn :done)
183 | (query-todos @!conn :all)
184 | (query-todos @!conn :active)
185 | (query-todos @!conn :done)
186 | (d/q '[:find (count ?e) . :where [?e :task/status]] @!conn))
187 |
--------------------------------------------------------------------------------
/src/user/demo_todomvc.md:
--------------------------------------------------------------------------------
1 | What's happening
2 |
3 | - styles
4 | - it's multiplayer! 0 LOC cost
5 | - state is durable (server side database) 0 LOC cost
6 | - managed busy states
7 |
8 | Key ideas
9 | - **entire app as a function**: with everything - frontend, backend, state, network, reactivity, dom rendering, resource lifecycle, load states. Unified into just functions.
10 |
11 | App as a function:
12 |
13 | ```clojure
14 | (e/defn TodoMVC-UI [state]
15 | (dom/section (dom/props {:class "todoapp"})
16 | (dom/header (dom/props {:class "header"})
17 | (CreateTodo.))
18 | (when (e/server (pos? (todo-count db :all)))
19 | (TodoList. state))
20 | (dom/footer (dom/props {:class "footer"})
21 | (TodoStats. state))))
22 | ```
23 |
24 | - `TodoMVC-UI` is truly a function, it follows function laws, it has all forms of scope, etc
25 | - How can we test this? If it's really a function, and it really composes, that means we can call it in composed ways, like for loops,
26 |
--------------------------------------------------------------------------------
/src/user/demo_todomvc_composed.cljc:
--------------------------------------------------------------------------------
1 | (ns user.demo-todomvc-composed
2 | (:require #?(:clj [datascript.core :as d])
3 | [hyperfiddle.electric :as e]
4 | [hyperfiddle.electric-dom2 :as dom]
5 | [hyperfiddle.electric-ui4 :as ui]
6 | [user.demo-todomvc :as todomvc]))
7 |
8 | #?(:clj (def !n (atom 1)))
9 |
10 | (e/defn PopoverCascaded [i F]
11 | (e/client
12 | (let [!focused (atom false) focused (e/watch !focused)]
13 | (dom/div (dom/props {:style {:position "absolute"
14 | :left (str (* i 40) "px")
15 | :top (str (-> i (* 40) (+ 60)) "px")
16 | :z-index (+ i (if focused 1000 0))}})
17 | (dom/on "mouseenter" (e/fn [_] (reset! !focused true)))
18 | (dom/on "mouseleave" (e/fn [_] (reset! !focused false)))
19 | (F.)))))
20 |
21 | (e/defn TodoMVC-composed []
22 | (e/client
23 | (let [state (e/watch todomvc/!state)
24 | n (e/server (e/watch !n))]
25 | (e/server
26 | (binding [todomvc/db (e/watch todomvc/!conn)
27 | todomvc/transact! (partial d/transact! todomvc/!conn)]
28 | (e/client
29 | (dom/link (dom/props {:rel :stylesheet, :href "/todomvc.css"}))
30 | (ui/range n (e/fn [v] (e/server (reset! !n v)))
31 | (dom/props {:min 1 :max 25 :step 1}))
32 | (dom/div (dom/props {:class "todomvc" :style {:position "relative"}})
33 |
34 | (e/for [i (range n)] ; <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
35 | (PopoverCascaded. i
36 | (e/fn []
37 | (todomvc/TodoMVC-UI. state)))))))))))
38 |
--------------------------------------------------------------------------------
/src/user/demo_todomvc_composed.md:
--------------------------------------------------------------------------------
1 | What's happening
2 |
3 | - The slider controls a for loop which calls the `TodoMVC-UI` function repeatedly
4 | - this demo deadlocks sometimes, refresh the page and drag the slider slower. (fix is blocked until the next major revision)
5 |
6 | Novel forms
7 | * none
8 |
9 | Key ideas
10 | - **network-transparent composition**: express your frontend/backend app logic using the full composition power of Lisp - higher order functions, closures, recursion, loops, everything. Where is all the web crap? GONE!
11 |
--------------------------------------------------------------------------------
/src/user/demo_todos_simple.cljc:
--------------------------------------------------------------------------------
1 | (ns user.demo-todos-simple
2 | (:require [contrib.str :refer [empty->nil]]
3 | #?(:clj [datascript.core :as d])
4 | [hyperfiddle.electric :as e]
5 | [hyperfiddle.electric-dom2 :as dom]
6 | [hyperfiddle.electric-ui4 :as ui]))
7 |
8 | #?(:clj (defonce !conn (d/create-conn {}))) ; database on server
9 | (e/def db) ; injected database ref; Electric defs are always dynamic
10 |
11 | (e/defn TodoItem [id]
12 | (e/server
13 | (let [e (d/entity db id)
14 | status (:task/status e)]
15 | (e/client
16 | (dom/li
17 | (ui/checkbox
18 | (case status :active false, :done true)
19 | (e/fn [v]
20 | (e/server
21 | (d/transact! !conn [{:db/id id
22 | :task/status (if v :done :active)}])
23 | nil))
24 | (dom/props {:id id}))
25 | (dom/label (dom/props {:for id})
26 | (dom/text (e/server (:task/description e)))))))))
27 |
28 | (e/defn InputSubmit [F]
29 | ; Custom input control using lower dom interface for Enter handling
30 | (e/client
31 | (dom/input (dom/props {:placeholder "Buy milk"})
32 | (dom/on "keydown" (e/fn [e]
33 | (when (= "Enter" (.-key e))
34 | (when-some [v (empty->nil (-> e .-target .-value))]
35 | (new F v)
36 | (set! (.-value dom/node) ""))))))))
37 |
38 | (e/defn TodoCreate []
39 | (e/client
40 | (InputSubmit. (e/fn [v]
41 | (e/server
42 | (d/transact! !conn [{:task/description v
43 | :task/status :active}])
44 | nil)))))
45 |
46 | #?(:clj (defn todo-count [db]
47 | (count
48 | (d/q '[:find [?e ...] :in $ ?status
49 | :where [?e :task/status ?status]]
50 | db :active))))
51 |
52 | #?(:clj (defn todo-records [db]
53 | (->> (d/q '[:find [(pull ?e [:db/id :task/description]) ...]
54 | :where [?e :task/status]] db)
55 | (sort-by :task/description))))
56 |
57 | (e/defn TodoList []
58 | (e/server
59 | (binding [db (e/watch !conn)]
60 | (e/client
61 | (dom/div (dom/props {:class "todo-list"})
62 | (TodoCreate.)
63 | (dom/ul (dom/props {:class "todo-items"})
64 | (e/server
65 | (e/for-by :db/id [{:keys [db/id]} (todo-records db)]
66 | (TodoItem. id))))
67 | (dom/p (dom/props {:class "counter"})
68 | (dom/span (dom/props {:class "count"})
69 | (dom/text (e/server (todo-count db))))
70 | (dom/text " items left")))))))
71 |
--------------------------------------------------------------------------------
/src/user/demo_todos_simple.md:
--------------------------------------------------------------------------------
1 | What's happening
2 | * It's a functional todo list, the first "real app"
3 | *
4 |
5 | Novel forms
6 | * `ui/checkbox`
7 | * `binding` – reactive dynamic scope; today all Electric defs are dynamic.
8 |
9 | Key ideas
10 | * dependency injection
11 | * dynamic scope
12 | * unserializable reference transfer - `d/transact!` returns an unserializable ref which cannot be moved over network, when this happens it is typically unintentional, so instead of crashing we warn and send `nil` instead.
13 | * nested transfers, even inside a loop
14 | * query diffing
15 |
--------------------------------------------------------------------------------
/src/user/demo_toggle.cljc:
--------------------------------------------------------------------------------
1 | (ns user.demo-toggle
2 | (:require [hyperfiddle.electric :as e]
3 | [hyperfiddle.electric-dom2 :as dom]
4 | [hyperfiddle.electric-ui4 :as ui]))
5 |
6 | #?(:clj (defonce !x (atom true))) ; server state
7 | (e/def x (e/server (e/watch !x))) ; reactive signal derived from atom
8 |
9 | (e/defn Toggle []
10 | (e/client
11 | (dom/div
12 | (dom/text "number type here is: "
13 | (case x
14 | true (e/client (pr-str (type 1))) ; javascript number type
15 | false (e/server (pr-str (type 1)))))) ; java number type
16 |
17 | (dom/div
18 | (dom/text "current site: "
19 | (if x
20 | "ClojureScript (client)"
21 | "Clojure (server)")))
22 |
23 | (ui/button (e/fn []
24 | (e/server (swap! !x not)))
25 | (dom/text "toggle client/server"))))
--------------------------------------------------------------------------------
/src/user/demo_toggle.md:
--------------------------------------------------------------------------------
1 | What's happening
2 | * There's a button on the frontend, with a callback, that toggles a boolean, stored in a server-side atom.
3 | * That boolean is used to switch between a client expr and a server expr.
4 | * Both exprs print the platform number type, which is either a `java.lang.Long` or a javascript `Number`.
5 | * The resulting string is streamed from server to client over network, and written through to the DOM.
6 |
7 | Novel forms
8 | * `e/def`: defines a reactive value `x`, the body is Electric code
9 | * `e/watch`: derives a reactive flow from a Clojure atom by watching for changes
10 | * `hyperfiddle.electric-ui4`: high level form controls (inputs, typeaheads, etc) with managed loading/syncing states
11 | * `ui/button`: a button with managed load state. On click, it becomes disabled until the callback result —possibly remote— is available.
12 | * `e/fn`: reactive lambda, supports client/server transfer
13 |
14 | Key Ideas
15 | * **distributed lambda**: the button callback spans both client and server. As does the `Toggle` function itself.
16 | * **"single state atom"** UI pattern, except the atom is on the server.
17 | * **reactive control flow**: `if`, `case` and other Clojure control flow forms are reactive. Here, when `x` toggles, `(case x)` will *switch* between branches. In the DAG, if-nodes look like a railroad switch:
18 | - 
19 | * **Clojure/Script interop**: The atom definition is ordinary Clojure code, which works because this is an ordinary `.cljc` file.
20 |
21 | Client/server value transfer
22 | * Only values can be serialized and moved across the network.
23 | * Reference types (e.g. atoms, database connections, Java classes) are unserializable and therefore cannot be moved.
24 | * Quiz: in `(e/server (pr-str (type 1)))`, why do we `pr-str` on the server? Hint: what type is `java.lang.Long`?
25 | * Quiz: in `(e/def x (e/server (e/watch !x)))`, why do we `e/watch` on the server?
26 |
27 | We target full Clojure/Script compatibility
28 | * That means, any valid Clojure expression, when pasted into an Electric body, will evaluate to the same result, and produce the same side effects, in the same order. Electric is additive Clojure, it takes nothing away.
29 | * To achieve this, Electric implements a proper Clojure/Script analyzer to support all Clojure special forms.
30 | * There are minor edge cases (especially as our compiler matures), mostly inconsequential.
31 | * macros work, side effects work, platform interop works, data structures are the same, clojure.core works
32 | * **It's just Clojure!**
--------------------------------------------------------------------------------
/src/user/demo_two_clocks.cljc:
--------------------------------------------------------------------------------
1 | (ns user.demo-two-clocks
2 | (:require [hyperfiddle.electric :as e]
3 | [hyperfiddle.electric-dom2 :as dom]))
4 |
5 | (e/defn TwoClocks []
6 | (e/client
7 | (let [c (e/client e/system-time-ms)
8 | s (e/server e/system-time-ms)]
9 |
10 | (dom/div (dom/text "client time: " c))
11 | (dom/div (dom/text "server time: " s))
12 | (dom/div (dom/text "difference: " (- s c))))))
--------------------------------------------------------------------------------
/src/user/demo_two_clocks.md:
--------------------------------------------------------------------------------
1 | What's happening
2 |
3 | * There are two reactive clocks, one on the frontend and one on the server
4 | * When a clock updates, the view incrementally recomputes to stay consistent
5 | * The server clock streams to the client over websocket
6 | * The expression is full-stack – it has frontend parts and backend parts
7 | * The Electric compiler infers the backend/frontend boundary and generates the full-stack app (client and server that coordinate)
8 | * Network sync is automatic and invisible
9 |
10 | Novel forms
11 |
12 | * `e/defn`: defines an Electric function, which is reactive. Electric fns follow all the same rules as ordinary Clojure functions.
13 | * `e/client`, `e/server`: compile time markers, valid in any Electric body
14 | * `e/system-time-ms`: reactive clock, defined with [Missionary](https://github.com/leonoel/missionary)
15 | * `hyperfiddle.electric-dom`: reactive DOM rendering combinators
16 |
17 | Key ideas
18 |
19 | * **multi-tier**: the `TwoClocks` function spans both frontend and backend, which are developed together in a single programming language and compilation unit. See [Multitier programming (wikipedia)](https://en.wikipedia.org/wiki/Multitier_programming)
20 | * **reactive**: the function body is reactive, keeping the DOM in sync with the clocks. Both the frontend and backend parts of the function are reactive.
21 | * **network-transparent**: the network is also reactive. the function transmits data over the network (as implied by the AST) in a way which is invisible to the application programmer. See: [Network transparency (wikipedia)](https://en.wikipedia.org/wiki/Network_transparency)
22 | * **streaming lexical scope**: this is not RPC (request/response), that would be too slow. The server streams `s` without being asked, because it knows the client depends on it. If the client had to request each server clock tick, the timer would pause visibly between each request.
23 | * **dom rendering is free**: Electric is already a general-purpose reactive engine, so electric-dom is nearly trivial, it's just 300 LOC of mostly syntax helpers only. There is neither virtual dom, reconcilier, nor diffing.
24 |
25 | Electric is a reactivity compiler
26 |
27 | * Electric has a DAG-based reactive evaluation model for fine-grained reactivity.
28 | * Electric uses macros to compile actual Clojure syntax into a DAG, using an actual Clojure/Script analyzer.
29 | * Unlike React.js, reactivity is granular to the expression level, not the function level.
30 | * (This is unlike React.js, where reactivity is granular to the function level.)
31 |
32 | Electric code is analyzed at the expression level.
33 |
34 | * `e/defn` defines functions that Electric can analyze the body of
35 | * Each expression, e.g. `(- s c)`, is a node in the DAG.
36 | * Each expression is async
37 | * Each expression is reactive
38 | * Arguments, e.g. `s`, is an edge in the DAG.
39 | * Expressions are recomputed when any argument updates.
40 |
41 | To visualize the DAG, node `(- s c)` has:
42 | * three incoming edges `#{- s c}`, and
43 | * one outgoing edge—the anonymous result—pointing to `(dom/text "skew: " _)`.
44 | * So, if `s` changes, `(- s c)` is recomputed using memoized `c`, and then the `dom/text` reruns (a point write).
45 |
46 | There is an isomorphism between programs and DAGs
47 |
48 | * you already knew this, if you think about it – see [call graph (wikipedia)](https://en.wikipedia.org/wiki/Call_graph)
49 | * The DAG is an abstract representation of a program
50 | * The DAG contains everything there is to know about the flow of data through the Electric program
51 | * Electric uses this DAG to drive reactivity, so we sometimes call the DAG a "reactivity graph".
52 | * But in theory, this DAG is abstract and there could be evaluated (interpreted or compiled) in many ways.
53 | * E.g., in addition to driving reactivity, Electric uses the DAG to drive network topology, which is just a graph coloring problem.
54 |
55 | Network is reactive at the granularity of individual scope values
56 | * when server clock `s` updates, the new value is streamed over network, bound to `s` on the client, ...
57 | * Everything is already async, so adding a 10ms websocket delay does not add impedance, complexity or code weight!
58 |
59 | For a 10min video explainer, see [UIs are streaming DAGs](https://hyperfiddle.notion.site/UIs-are-streaming-DAGs-e181461681a8452bb9c7a9f10f507991).
--------------------------------------------------------------------------------
/src/user/demo_virtual_scroll.cljc:
--------------------------------------------------------------------------------
1 | (ns user.demo-virtual-scroll
2 | (:require [contrib.data :refer [unqualify]]
3 | [hyperfiddle.electric :as e]
4 | [hyperfiddle.electric-dom2 :as dom]
5 | [hyperfiddle.electric-ui4 :as ui]
6 | #?(:cljs goog.object)))
7 |
8 | (e/defn DemoFixedHeightCounted
9 | "Scrolls like google sheets. this can efficiently jump through a large indexed collection"
10 | []
11 | (let [row-count 500
12 | xs (vec (range row-count)) ; counted
13 | page-size 100
14 | row-height 22] ; todo use relative measurement (browser zoom impacts px height)
15 | (e/client
16 | (dom/div (dom/props {:class "viewport" :style {:overflowX "hidden" :overflowY "auto"}})
17 | (let [[scrollTop] (new (ui/scroll-state< dom/node))
18 | max-height (* row-count row-height)
19 | clamped-scroll-top (js/Math.min scrollTop max-height)
20 | start (/ clamped-scroll-top row-height)] ; (js/Math.floor)
21 | (dom/div (dom/props {:style {:height (str (* row-height row-count) "px") ; optional absolute scrollbar
22 | :padding-top (str clamped-scroll-top "px") ; seen elements are replaced with padding
23 | :padding-bottom (str (- max-height clamped-scroll-top) "px")}})
24 | (e/server
25 | ; seen elements are unmounted
26 | (e/for [x #_(subvec xs
27 | (Math/min start row-count)
28 | (Math/min (+ start page-size) row-count))
29 | (->> xs (drop start) (take page-size))]
30 | (e/client (dom/div (dom/text x)))))))))))
31 |
32 | (e/defn DemoVariableHeightInfinite
33 | "Scrolls like newsfeed. Natural browser layout for variable height rows. Leaves seen elements
34 | mounted in the DOM."
35 | []
36 | (let [xs (range) ; infinite
37 | page-size 100]
38 | (e/client
39 | (dom/div (dom/props {:class "viewport"})
40 | (let [!pages (atom 1) pages (e/watch !pages)
41 | [scrollTop scrollHeight clientHeight] (new (ui/scroll-state< dom/node))]
42 | (when (>= scrollTop (- scrollHeight clientHeight clientHeight)) ; scrollThresholdPx = clientHeight
43 | (swap! !pages inc)) ; can this get spammed by Electric?
44 | (dom/div ; content is unstyled, uses natural layout
45 | (e/server
46 | (e/for [x (->> xs (take (* pages page-size)))] ; leave dom
47 | (e/client (dom/div (dom/text x)))))))))))
48 |
49 | #?(:clj (defonce !demo (atom {:text "DemoFixedHeightCounted" ::value `DemoFixedHeightCounted})))
50 |
51 | (e/def demo (e/server (e/watch !demo)))
52 |
53 | (e/def demos {`DemoVariableHeightInfinite DemoVariableHeightInfinite
54 | `DemoFixedHeightCounted DemoFixedHeightCounted})
55 |
56 | (e/defn VirtualScroll []
57 | (e/client
58 | ; Requires css {box-sizing: border-box;}
59 | (dom/element "style" (dom/text ".header { position: fixed; z-index:1; top: 0; left: 0; right: 0; height: 100px; background-color: #abcdef; }"
60 | ".footer { position: fixed; bottom: 0; left: 0; right: 0; height: 100px; background-color: #abcdef; }"
61 | ".viewport { position: fixed; top: 100px; bottom: 100px; left: 0; right: 0; background-color: #F63; overflow: auto; }"))
62 | (dom/div (dom/props {:class "header"})
63 | (dom/dl
64 | (dom/dt (dom/text "scroll debug state"))
65 | (dom/dd (dom/pre (dom/text (pr-str (update-keys (e/watch ui/!scrollStateDebug) unqualify))))))
66 | (e/server
67 | (ui/select
68 | demo
69 | (e/fn V! [v] (reset! !demo v))
70 | (e/fn Options [] [{:text "DemoFixedHeightCounted" ::value `DemoFixedHeightCounted}
71 | {:text "DemoVariableHeightInfinite" ::value `DemoVariableHeightInfinite}])
72 | (e/fn OptionLabel [x] (:text x)))))
73 | (e/server (new (get demos (::value demo))))
74 | (dom/div (dom/props {:class "footer"})
75 | (dom/text "Try scrolling to the top, and resizing the window."))))
76 |
--------------------------------------------------------------------------------
/src/user/demo_webview.cljc:
--------------------------------------------------------------------------------
1 | (ns user.demo-webview
2 | (:require #?(:clj [datascript.core :as d])
3 | [hyperfiddle.electric :as e]
4 | [hyperfiddle.electric-dom2 :as dom]
5 | [hyperfiddle.electric-ui4 :as ui]))
6 |
7 | #?(:clj
8 | (defonce conn ; state survives reload
9 | (doto (d/create-conn {:order/email {}})
10 | (d/transact! ; test data
11 | [{:order/email "alice@example.com" :order/gender :order/female}
12 | {:order/email "bob@example.com" :order/gender :order/male}
13 | {:order/email "charlie@example.com" :order/gender :order/male}]))))
14 |
15 | #?(:clj
16 | (defn teeshirt-orders [db ?email]
17 | (sort
18 | (d/q '[:find [?e ...]
19 | :in $ ?needle :where
20 | [?e :order/email ?email]
21 | [(clojure.string/includes? ?email ?needle)]]
22 | db (or ?email "")))))
23 |
24 | (e/defn Teeshirt-orders-view [db]
25 | (e/client
26 | (dom/div
27 | (let [!search (atom ""), search (e/watch !search)]
28 | (ui/input search (e/fn [v] (reset! !search v))
29 | (dom/props {:placeholder "Filter..."}))
30 | (dom/table (dom/props {:class "hyperfiddle"})
31 | (e/server
32 | (e/for [id (teeshirt-orders db search)]
33 | (let [!e (d/entity db id)]
34 | (e/client
35 | (dom/tr
36 | (dom/td (dom/text id))
37 | (dom/td (dom/text (e/server (:order/email !e))))
38 | (dom/td (dom/text (e/server (:order/gender !e))))))))))))))
39 |
40 | (e/defn Webview []
41 | (e/server
42 | (let [db (e/watch conn)] ; reactive "database value"
43 | (Teeshirt-orders-view. db))))
44 |
45 | (comment
46 | #?(:clj (d/transact conn [{:db/id 2 :order/email "bob2@example.com"}]))
47 | #?(:clj (d/transact conn [{:order/email "dan@example.com"}]))
48 | #?(:clj (d/transact conn [{:order/email "erin@example.com"}]))
49 | #?(:clj (d/transact conn [{:order/email "frank@example.com"}]))
50 | )
51 |
--------------------------------------------------------------------------------
/src/user/demo_webview.md:
--------------------------------------------------------------------------------
1 | What's happening
2 | * The webview is subscribed to the database, which updates with each transaction.
3 | * If you ran the transact forms at the bottom in your REPL, the view would update reactively.
4 |
5 | Novel forms
6 | * `e/watch` on datascript connection
7 |
8 | Key ideas
9 | * Datascript is on the server, it can be any database
10 | * Direct query/view composition, with a loop
--------------------------------------------------------------------------------
/src/user/example_datascript_db.clj:
--------------------------------------------------------------------------------
1 | ;; An example databsae of tee-shirt orders
2 | ;; server side only - for demos
3 | (ns user.example-datascript-db
4 | (:require [datascript.core :as d]
5 | [datascript.impl.entity :as de] ; for `entity?` predicate
6 | [hyperfiddle.api :as hf]
7 | [hyperfiddle.rcf :refer [tests % tap]]))
8 |
9 | (def schema ; user orders a tee-shirt and select a tee-shirt gender and size + optional tags
10 | ;; :hf/valueType is an annotation, used by hyperfiddle to auto render a UI (To be improved. Datascript only accepts :db/valueType on refs)
11 | {:order/email {:hf/valueType :db.type/string, :db/cardinality :db.cardinality/one, :db/unique :db.unique/identity}
12 | :order/gender {:db/valueType :db.type/ref, :db/cardinality :db.cardinality/one}
13 | :order/shirt-size {:db/valueType :db.type/ref, :db/cardinality :db.cardinality/one}
14 | :order/type {:db/cardinality :db.cardinality/one}
15 | :order/tags {:db/cardinality :db.cardinality/many}
16 | :db/ident {:db/unique :db.unique/identity, :hf/valueType :db.type/keyword}})
17 |
18 | (defn fixtures [db]
19 | (-> db
20 | ;; Add tee-shirt types
21 | (d/with [{:db/id 1, :order/type :order/gender, :db/ident :order/male} ; straight cut
22 | {:db/id 2, :order/type :order/gender, :db/ident :order/female}]) ; fitted
23 | :db-after
24 | ;; Add tee-shirt sizes by type
25 | (d/with [{:db/id 3 :order/type :order/shirt-size :db/ident :order/mens-small :order/gender :order/male}
26 | {:db/id 4 :order/type :order/shirt-size :db/ident :order/mens-medium :order/gender :order/male}
27 | {:db/id 5 :order/type :order/shirt-size :db/ident :order/mens-large :order/gender :order/male}
28 | {:db/id 6 :order/type :order/shirt-size :db/ident :order/womens-small :order/gender :order/female}
29 | {:db/id 7 :order/type :order/shirt-size :db/ident :order/womens-medium :order/gender :order/female}
30 | {:db/id 8 :order/type :order/shirt-size :db/ident :order/womens-large :order/gender :order/female}])
31 | :db-after
32 | ;; Add example orders
33 | (d/with [{:db/id 9, :order/email "alice@example.com", :order/gender :order/female, :order/shirt-size :order/womens-large, :order/tags [:a :b :c]}
34 | {:db/id 10, :order/email "bob@example.com", :order/gender :order/male, :order/shirt-size :order/mens-large, :order/tags [:b]}
35 | {:db/id 11, :order/email "charlie@example.com", :order/gender :order/male, :order/shirt-size :order/mens-medium}])
36 | :db-after))
37 |
38 | (declare conn)
39 |
40 | (defn setup-db! []
41 | (def conn (d/create-conn schema))
42 | (alter-var-root #'hf/*$* (constantly (fixtures (d/db conn)))))
43 |
44 | (setup-db!)
45 |
46 |
47 | (def db hf/*$*) ; for @(requiring-resolve 'user.example-datascript-db/db)
48 |
49 | (defn get-schema [db a] (get (:schema db) a))
50 |
51 | (defn nav!
52 | ([_ e] e)
53 | ([db e a] (let [v (a (if (de/entity? e) e (d/entity db e)))]
54 | (if (de/entity? v)
55 | (or (:db/ident v) (:db/id v))
56 | v)))
57 | ([db e a & as] (reduce (partial nav! db) (nav! db e a) as)))
58 |
59 | (def male 1 #_:order/male #_17592186045418)
60 | (def female 2 #_:order/female #_17592186045419)
61 | (def m-sm 3 #_17592186045421)
62 | (def m-md 4 #_nil)
63 | (def m-lg 5 #_nil)
64 | (def w-sm 6 #_nil)
65 | (def w-md 7 #_nil)
66 | (def w-lg 8 #_nil)
67 | (def alice 9 #_17592186045428)
68 | (def bob 10 #_nil)
69 | (def charlie 11 #_nil)
70 |
71 | (comment
72 | (hyperfiddle.rcf/enable!))
73 |
74 | (tests
75 | (def e [:order/email "alice@example.com"])
76 |
77 | (tests
78 | "(d/pull ['*]) is best for tests"
79 | (d/pull db ['*] e)
80 | := {:db/id 9,
81 | :order/email "alice@example.com",
82 | :order/shirt-size #:db{:id 8},
83 | :order/gender #:db{:id 2}
84 | :order/tags [:a :b :c]})
85 |
86 | (comment #_tests
87 | "careful, entity type is not= to equivalent hashmap"
88 | (d/touch (d/entity db e))
89 | ; expected failure
90 | := {:order/email "alice@example.com",
91 | :order/gender #:db{:id 2},
92 | :order/shirt-size #:db{:id 8},
93 | :order/tags #{:c :b :a},
94 | :db/id 9})
95 |
96 | (tests
97 | "entities are not maps"
98 | (type (d/touch (d/entity db e)))
99 | *1 := datascript.impl.entity.Entity) ; not a map
100 |
101 | (comment #_tests
102 | "careful, entity API tests are fragile and (into {}) is insufficient"
103 | (->> (d/touch (d/entity db e)) ; touch is the best way to inspect an entity
104 | (into {})) ; but it's hard to convert to a map...
105 | := #:order{#_#_:id 9 ; db/id is not present!
106 | :email "alice@example.com",
107 | :gender _ #_#:db{:id 2}, ; entity ref not =, RCF can’t unify with entities
108 | :shirt-size _ #_#:db{:id 8}, ; entity ref not =
109 | :tags #{:c :b :a}}
110 |
111 | "select keys doesn't fix the problem as it's not recursive"
112 | (-> (d/touch (d/entity db e))
113 | (select-keys [:order/email :order/shirt-size :order/gender]))
114 | := #:order{:email "alice@example.com",
115 | :shirt-size _ #_#:db{:id 8}, ; still awkward, need recursive pull
116 | :gender _ #_#:db{:id 2}}) ; RCF can’t unify with an entities
117 |
118 | "TLDR is use (d/pull ['*]) like the first example"
119 | (tests
120 | (d/pull db ['*] :order/female)
121 | := {:db/id female :db/ident :order/female :order/type :order/gender})
122 |
123 | (tests
124 | (d/q '[:find [?e ...] :where [_ :order/gender ?e]] db)
125 | := [2 1] #_[:order/male :order/female])
126 | )
127 |
--------------------------------------------------------------------------------
/src/user/tutorial_7guis_1_counter.cljc:
--------------------------------------------------------------------------------
1 | (ns user.tutorial-7guis-1-counter
2 | (:require [hyperfiddle.electric :as e]
3 | [hyperfiddle.electric-dom2 :as dom]
4 | [hyperfiddle.electric-ui4 :as ui]))
5 |
6 | (e/defn Counter []
7 | (e/client
8 | (let [!state (atom 0)]
9 | (dom/p (dom/text (e/watch !state)))
10 | (ui/button (e/fn [] (swap! !state inc))
11 | (dom/text "Count")))))
--------------------------------------------------------------------------------
/src/user/tutorial_7guis_2_temperature.cljc:
--------------------------------------------------------------------------------
1 | (ns user.tutorial-7guis-2-temperature
2 | (:require clojure.math
3 | [hyperfiddle.electric :as e]
4 | [hyperfiddle.electric-dom2 :as dom]
5 | [hyperfiddle.electric-ui4 :as ui]
6 | [missionary.core :as m]))
7 |
8 | (defn c->f [c] (+ (* c (/ 9 5)) 32))
9 | (defn f->c [f] (* (- f 32) (/ 5 9)))
10 | (defn random-value [_] (m/sp (m/? (m/sleep 1000)) (rand-int 40)))
11 |
12 | (e/defn TemperatureConverter []
13 | (e/client
14 | (let [!t (atom 0), t (e/watch !t)]
15 | (reset! !t (new (e/task->cp (random-value t)))) ; test concurrent updates
16 | (dom/dl
17 | (dom/dt (dom/text "Celsius"))
18 | (dom/dd (ui/long (clojure.math/round t)
19 | (e/fn [v] (reset! !t v))))
20 | (dom/dt (dom/text "Farenheit"))
21 | (dom/dd (ui/long (clojure.math/round (c->f t))
22 | (e/fn [v] (reset! !t (f->c v)))))))))
--------------------------------------------------------------------------------
/src/user/tutorial_7guis_4_timer.cljc:
--------------------------------------------------------------------------------
1 | (ns user.tutorial-7guis-4-timer
2 | (:require [hyperfiddle.electric :as e]
3 | [hyperfiddle.electric-dom2 :as dom]
4 | [hyperfiddle.electric-ui4 :as ui4]))
5 |
6 | (def initial-goal 10)
7 | (defn seconds [milliseconds] (/ (Math/floor (/ milliseconds 100)) 10))
8 | (defn second-precision [milliseconds]
9 | (-> milliseconds (/ 1000) (Math/floor) (* 1000))) ; drop milliseconds
10 |
11 | (defn now [] #?(:cljs (second-precision (js/Date.now))))
12 |
13 | (e/defn Timer []
14 | (e/client
15 | (let [!goal (atom initial-goal)
16 | !start (atom (now))
17 | goal (e/watch !goal)
18 | goal-ms (* 1000 goal)
19 | start (e/watch !start)
20 | time (min goal-ms (- (second-precision e/system-time-ms) start))]
21 | (dom/div (dom/props {:style {:display :grid ;:margin-left "20rem"
22 | :width "20em"
23 | :grid-gap "0 1rem"
24 | :align-items :center}})
25 | (dom/span (dom/text "Elapsed Time:"))
26 | (dom/progress (dom/props {:max goal-ms, :value time, :style {:grid-column 2}}))
27 | (dom/span (dom/text (seconds time) " s"))
28 | (dom/span (dom/props {:style {:grid-row 3}}) (dom/text "Duration: " goal "s"))
29 | (ui4/range goal (e/fn [v] (reset! !goal v))
30 | (dom/props {:min 0, :max 60, :style {:grid-row 3}}))
31 | (ui4/button (e/fn [] (reset! !start (now)))
32 | (dom/props {:style {:grid-row 4, :grid-column "1/3"}})
33 | (dom/text "Reset"))))))
34 |
--------------------------------------------------------------------------------
/src/user/tutorial_7guis_5_crud.cljc:
--------------------------------------------------------------------------------
1 | (ns user.tutorial-7guis-5-crud
2 | (:require [clojure.string :as str]
3 | [hyperfiddle.electric :as e]
4 | [hyperfiddle.electric-dom2 :as dom]
5 | [hyperfiddle.electric-ui4 :as ui4]))
6 |
7 | (def !state (atom {:selected nil
8 | :stage {:name ""
9 | :surname ""}
10 | :names (sorted-map 0 {:name "Emil", :surname "Hans"})}))
11 |
12 | (def next-id (partial swap! (atom 0) inc))
13 |
14 | (defn select! [id]
15 | (swap! !state (fn [state]
16 | (assoc state :selected id
17 | :stage (get-in state [:names id])))))
18 |
19 | (defn set-name! [name]
20 | (swap! !state assoc-in [:stage :name] name))
21 |
22 | (defn set-surname! [surname]
23 | (swap! !state assoc-in [:stage :surname] surname))
24 |
25 | (defn create! []
26 | (swap! !state (fn [{:keys [stage] :as state}]
27 | (-> state
28 | (update :names assoc (next-id) stage)
29 | (assoc :stage {:name "", :surname ""})))))
30 | (defn delete! []
31 | (swap! !state (fn [{:keys [selected] :as state}]
32 | (update state :names dissoc selected))))
33 |
34 | (defn update! []
35 | (swap! !state (fn [{:keys [selected stage] :as state}]
36 | (assoc-in state [:names selected] stage))))
37 |
38 | (defn filter-names [names-map needle]
39 | (if (empty? needle)
40 | names-map
41 | (let [needle (str/lower-case needle)]
42 | (reduce-kv (fn [r k {:keys [name surname]}]
43 | (if (or (str/includes? (str/lower-case name) needle)
44 | (str/includes? (str/lower-case surname) needle))
45 | r
46 | (dissoc r k)))
47 | names-map names-map))))
48 |
49 | (e/defn CRUD []
50 | (e/client
51 | (let [state (e/watch !state)
52 | selected (:selected state)]
53 | (dom/div (dom/props {:style {:display :grid
54 | :grid-gap "0.5rem"
55 | :align-items :baseline
56 | :grid-template-areas "'a b c c'\n
57 | 'd d e f'\n
58 | 'd d g h'\n
59 | 'd d i i'\n
60 | 'j j j j'"}})
61 | (dom/span (dom/props {:style {:grid-area "a"}})
62 | (dom/text "Filter prefix:"))
63 | (let [!needle (atom ""), needle (e/watch !needle)]
64 | (ui4/input needle (e/fn [v] (reset! !needle v))
65 | (dom/props {:style {:grid-area "b"}}))
66 | (dom/ul (dom/props {:style {:grid-area "d"
67 | :background-color :white
68 | :list-style-type :none
69 | :padding 0
70 | :border "1px gray solid"
71 | :height "100%"}})
72 | (e/for [entry (filter-names (:names state) needle)]
73 | (let [id (key entry)
74 | value (val entry)]
75 | (dom/li (dom/text (:surname value) ", " (:name value))
76 | (dom/props
77 | {:style {:cursor :pointer
78 | :color (if (= selected id) :white :inherit)
79 | :background-color (if (= selected id) :blue :inherit)
80 | :padding "0.1rem 0.5rem"}})
81 | (dom/on "click" (e/fn [_] (select! id))))))))
82 | (let [stage (:stage state)]
83 | (dom/span (dom/props {:style {:grid-area "e"}}) (dom/text "Name:"))
84 | (ui4/input (:name stage) (e/fn [v] (set-name! v))
85 | (dom/props {:style {:grid-area "f"}}))
86 | (dom/span (dom/props {:style {:grid-area "g"}}) (dom/text "Surname:"))
87 | (ui4/input (:surname stage) (e/fn [v] (set-surname! v))
88 | (dom/props {:style {:grid-area "h"}})))
89 | (dom/div (dom/props
90 | {:style {:grid-area "j"
91 | :display :grid
92 | :grid-gap "0.5rem"
93 | :grid-template-columns "auto auto auto 1fr"}})
94 | (ui4/button (e/fn [] (create!)) (dom/text "Create"))
95 | (ui4/button (e/fn [] (update!)) (dom/text "Update")
96 | (dom/props {:disabled (not selected)}))
97 | (ui4/button (e/fn [] (delete!)) (dom/text "Delete")
98 | (dom/props {:disabled (not selected)})))))))
99 |
--------------------------------------------------------------------------------
/src/user/tutorial_backpressure.cljc:
--------------------------------------------------------------------------------
1 | (ns user.tutorial-backpressure
2 | (:require [hyperfiddle.electric :as e]
3 | [hyperfiddle.electric-dom2 :as dom]))
4 |
5 | (e/defn Backpressure []
6 | (e/client
7 | (let [c (e/client e/system-time-secs)
8 | s (e/server (double e/system-time-secs))]
9 |
10 | (println "s" (int s))
11 | (println "c" (int c))
12 |
13 | (dom/div (dom/text "client time: " c))
14 | (dom/div (dom/text "server time: " s))
15 | (dom/div (dom/text "difference: " (.toPrecision (- s c) 2))))))
--------------------------------------------------------------------------------
/src/user/tutorial_backpressure.md:
--------------------------------------------------------------------------------
1 | What's happening
2 | * The timer `e/system-time-secs` is a float and updates at the browser animation rate, let's say 120hz.
3 | * Clocks are printed to both the DOM (at 120hz) and also the browser console (at 1hz)
4 | * The clocks pause when the browser page is not visible (i.e. you switch tabs), confirm it
5 |
6 | Novel forms
7 | * None
8 |
9 | Key ideas
10 | * **work-skipping**: expressions are only recomputed if an argument actually changes, otherwise the previous value is reused. Here, the truncated clock timer does not needlessly spam the println; downstream nodes are only recomputed on the transition.
11 | * **signals**: Electric reactive functions model *signals*, not *streams*. Streams are like mouse clicks or database transactions – you can't skip one. Signals are like mouse coordinates, audio signals or game animations – you mostly only care about latest, nobody wants an animation frame from 2 seconds ago.
12 | * **the reactive clocks are lazy**: the clocks do not tick until the previous tick was *sampled* (i.e. consumed). That means if the rendering process falls behind the requestAnimationFrame tick rate, it will simply skip frames like a video game.
13 | * **lazy sampling**: All electric expressions are lazily sampled, not just the clocks. Electric fns don't compute anything at all until sampled by the Electric entrypoint.
14 |
15 | Clock details
16 | * `e/system-time-secs` is implemented as a missionary flow.
17 | * on the client, the underlying clock is implemented with `requestAnimationFrame` such that it only schedules work once the current tick has been sampled.
18 | * That means, if you switch to another tab, the browser will stop scheduling animation frames and the clock will pause.
19 | * The server clock is implemented similarly, it will tick as fast as possible but only when sampled. If nobody is consuming the clock — i.e. no websockets are connected — the clock will not tick at all!
20 |
21 | Work-skipping
22 | * We use `int` to truncate the precision. The truncation is performed at 120hz, as we want to switch *precisely* on the rising edge of the transition.
23 | * Since `int` is recomputing at 120hz, that means anything immediately downstream will be checked 120 times per second
24 | * so both `(println "s" (int s))` and `(- s c)` are both checked at 120hz
25 | * However, expressions only run if at least one argument has changed
26 | * So, at this point the memoization kicks in and we will skip the work, this is called "work-skipping"
27 | * `(println "s" (int s))` prints at 1hz not 120hz
28 |
29 | Lazy sampling
30 | * Q: Truncating at 120hz is inefficient right? Why not use `(js/setTimeout 1000)` to make a slower clock?
31 | * A: Actually, the fast clock is perfectly efficient due to Electric's backpressure.
32 | * If the rendering process can't keep up with the requestAnimationFrame tick rate, it will simply skip frames, and this will actually slow down the clock because the clock itself is lazy too.
33 | * This is a form of backpressure. See [Streams vs Signals](https://www.dustingetz.com/#/page/signals%20vs%20streams%2C%20in%20terms%20of%20backpressure%20%282023%29)
34 | * Note: Computers can do 3D transforms in realtime, running this little clock at the browser animation rate is negligible.
35 | * If your computer is powered on and plugged in, you generally want to animate at the highest framerate the device is capable of. This is Electric's default behavior out of the box.
36 |
37 | Backpressure
38 | * What if you're on an old mobile device that can't keep up with the server clock streaming?
39 | * In this case, incoming messages from server will saturate the websocket buffer,
40 | * then the browser will propagate backpressure at the TCP layer,
41 | * then the server will decrease the *sampling rate*.
42 | * Same behavior in the opposite direction (e.g if the client generates lots of events and the server is overloaded).
43 | * See: [Signals vs Streams, in terms of backpressure](https://www.dustingetz.com/#/page/signals%20vs%20streams%2C%20in%20terms%20of%20backpressure%20%282023%29)
44 | * Electric is a Clojure to [Missionary](https://github.com/leonoel/missionary) compiler; it compiles lifts each Clojure form into a Missionary continuous flow.
45 | * Thereby automatically backpressuring every point in the Electric DAG.
46 | * If you want to customize the backpressure locally at any point — maybe you want to run a chat view with realtime network but run a slow query less aggressively — you can drop down into missionary's rich suite of concurrency combinators.
47 |
--------------------------------------------------------------------------------
/src/user/tutorial_lifecycle.cljc:
--------------------------------------------------------------------------------
1 | (ns user.tutorial-lifecycle
2 | (:require [hyperfiddle.electric :as e]
3 | [hyperfiddle.electric-dom2 :as dom]))
4 |
5 | (e/defn BlinkerComponent []
6 | (e/client
7 | (dom/h1 (dom/text "blink!"))
8 | (println 'component-did-mount)
9 | (e/on-unmount #(println 'component-will-unmount))))
10 |
11 | (e/defn Lifecycle []
12 | (e/client
13 | (if (= 0 (int (mod e/system-time-secs 2)))
14 | (BlinkerComponent.))))
15 |
--------------------------------------------------------------------------------
/src/user/tutorial_lifecycle.md:
--------------------------------------------------------------------------------
1 | What's happening
2 |
3 | * The string "blink!" is being mounted/unmounted every 2 seconds
4 | * The mount/unmount "component lifecycle" is logged to the browser console with `println`
5 | * `(BlinkerComponent.)` is being constructed and destructed
6 |
7 | Novel forms
8 |
9 | * `new`: calls an `e/fn` or `e/defn`. Here, `(BlinkerComponent.)` is desugared by Clojure to `(new BlinkerComponent)`, these two forms are identical.
10 | * `e/on-unmount` : takes a regular (non-reactive) function to run before unmount.
11 | * Why no `e/mount`? The `println` here runs on mount without extra syntax needed, we'd like to see a concrete use case not covered by this.
12 |
13 | Key ideas
14 |
15 | * **Electric functions have object lifecycle**: Reactive expressions have a "mount" and "unmount" lifecycle. `println` here runs on "mount" and never again since it has only constant arguments, unless the component is destroyed and recreated.
16 | * **Call Electric fns with `new`**: Reagent has ctor syntax too; in Reagent we call components with square brackets. This syntax distinguishes between calling Electric fns vs ordinary Clojure fns. To help remember, we capitalize Electric functions, same as Reagent/React components.
17 | * **Electric fns are both functions and objects**: They compose as functions, they have object lifecycle, and they have state. From here on we will refer to Electric fns as both "function" or "object" as appropriate, depending on which aspects are under discussion. We also sometimes refer to "calling" Electric fns as "booting" or "mounting".
18 | * **DAG as a value**: Electric lambdas `e/fn` are values, these can be thought of as "higher order DAGs", "DAG values" or "pieces of DAG".
19 | * **process supervision**
20 |
21 | New
22 |
23 | * Electric `new` is backwards compatible with Clojure/Script's new; if you pass it a class it will do the right thing.
24 | * Q: Why do we need syntax to call Electric fns, why not just use metadata on the var? A: Because lambdas.
25 | * Electric expressions can call both Electric lambdas and ordinary Clojure lambdas (like the sharp-lambda passed to e-unmount).
26 | * Due to Clojure being dynamically typed, there's no static information available for the compiler to infer the right call convention in this case.
27 | * That's why Reagent uses `[F]` and Electric uses `(F.)`. Note both capitalize `F`!
28 |
29 | Dynamic extent
30 |
31 | - Electric objects have *dynamic extent*.
32 | - "Dynamic extent refers to things that exist for a fixed period of time and are explicitly “destroyed” at the end of that period, usually when control returns to the code that created the thing." — from [On the Perils of Dynamic Scope (Sierra 2013)](https://stuartsierra.com/2013/03/29/perils-of-dynamic-scope)
33 | - Like [RAII](https://en.wikipedia.org/wiki/Resource_acquisition_is_initialization), this lifecycle is deterministic and intended for performing resource management effects.
34 |
35 | Process supervision
36 | - Electric `if` and other control flow nodes will mount and unmount their child branches (like switching the railroad track).
37 | - If an `e/fn` were to be booted inside of an `if`, the lifetime of the booted lambda is the duration for which the branch of the `if` is active.
38 | - Electric objects can manage references (e.g. DOM node or atom in lexical scope).
39 | - A managed reference's lifetime is tied to the supervising object's lifetime.
40 |
41 | Object state
42 |
43 | - Recall that Electric functions are auto-memoized. This memo buffer can be seen as the *object state*.
44 | - The memo buffer is discarded and reset when this happens.
45 | - In other words: Electric flows are not [*history sensitive*](https://blog.janestreet.com/breaking-down-frp/). (I hesitate to link to this article from 2014 because it contains confusion/FUD around the importance of continuous time, but the coverage of history sensitivity is good.)
--------------------------------------------------------------------------------
/src/user_main.cljc:
--------------------------------------------------------------------------------
1 | (ns user-main
2 | (:require clojure.edn
3 | clojure.string
4 | contrib.data
5 | contrib.ednish
6 | contrib.uri ; data_readers
7 | #?(:clj markdown.core)
8 | [contrib.electric-codemirror :refer [CodeMirror]]
9 | [hyperfiddle.electric :as e]
10 | [hyperfiddle.electric-dom2 :as dom]
11 | [hyperfiddle.electric-svg :as svg]
12 | [hyperfiddle.history :as history]
13 | user.demo-index
14 |
15 | user.demo-two-clocks
16 | user.demo-toggle
17 | user.demo-system-properties
18 | user.demo-chat
19 | user.demo-chat-extended
20 | user.demo-webview
21 | user.demo-todomvc
22 | user.demo-todomvc-composed
23 |
24 | user.demo-explorer
25 | #_wip.demo-explorer2
26 | user.demo-10k-dom
27 | user.demo-svg
28 | user.demo-todos-simple
29 | user.tutorial-7guis-1-counter
30 | user.tutorial-7guis-2-temperature
31 | user.tutorial-7guis-4-timer
32 | user.tutorial-7guis-5-crud
33 | user.demo-virtual-scroll
34 | user.demo-color
35 | user.demo-tic-tac-toe
36 | user.tutorial-lifecycle
37 | user.tutorial-backpressure
38 | #_wip.demo-branched-route
39 | #_wip.hfql
40 | wip.tag-picker
41 | #_wip.teeshirt-orders ; sensitive to electric dep, HFQL moved
42 | wip.demo-custom-types
43 | wip.tracing
44 | user.demo-reagent-interop ; yarn
45 | wip.demo-stage-ui4 ; yarn
46 | wip.js-interop))
47 |
48 | (e/defn NotFoundPage []
49 | (e/client (dom/h1 (dom/text "Page not found"))))
50 |
51 | ; todo: macro to auto-install demos by attaching clj metadata to e/defn vars?
52 |
53 | (e/def pages
54 | {`user.demo-two-clocks/TwoClocks user.demo-two-clocks/TwoClocks
55 | `user.demo-toggle/Toggle user.demo-toggle/Toggle
56 | `user.demo-system-properties/SystemProperties user.demo-system-properties/SystemProperties
57 | `user.demo-chat/Chat user.demo-chat/Chat
58 | `user.tutorial-backpressure/Backpressure user.tutorial-backpressure/Backpressure
59 | `user.tutorial-lifecycle/Lifecycle user.tutorial-lifecycle/Lifecycle
60 | `user.demo-chat-extended/ChatExtended user.demo-chat-extended/ChatExtended
61 | `user.demo-webview/Webview user.demo-webview/Webview
62 | `user.demo-todos-simple/TodoList user.demo-todos-simple/TodoList
63 | `user.demo-reagent-interop/ReagentInterop user.demo-reagent-interop/ReagentInterop
64 | ;; `wip.demo-stage-ui4/CrudForm wip.demo-stage-ui4/CrudForm
65 | `user.demo-svg/SVG user.demo-svg/SVG
66 | ; -- `wip.tracing/TracingDemo wip.tracing/TracingDemo
67 | `wip.demo-custom-types/CustomTypes wip.demo-custom-types/CustomTypes
68 |
69 | ; 7 GUIs
70 | `user.tutorial-7guis-1-counter/Counter user.tutorial-7guis-1-counter/Counter
71 | `user.tutorial-7guis-2-temperature/TemperatureConverter user.tutorial-7guis-2-temperature/TemperatureConverter
72 | `user.tutorial-7guis-4-timer/Timer user.tutorial-7guis-4-timer/Timer
73 | `user.tutorial-7guis-5-crud/CRUD user.tutorial-7guis-5-crud/CRUD
74 |
75 | ; Demos
76 | ;; `user.demo-todomvc/TodoMVC user.demo-todomvc/TodoMVC
77 | ;; `user.demo-todomvc-composed/TodoMVC-composed user.demo-todomvc-composed/TodoMVC-composed
78 | ;; `user.demo-explorer/DirectoryExplorer user.demo-explorer/DirectoryExplorer
79 | ;-- `wip.datomic-browser/DatomicBrowser wip.datomic-browser/DatomicBrowser -- separate repo now, should it come back?
80 | ; `user.demo-color/Color user.demo-color/Color
81 | ; -- user.demo-10k-dom/Dom-10k-Elements user.demo-10k-dom/Dom-10k-Elements ; todo too slow to unmount, crashes
82 |
83 | ; Hyperfiddle demos
84 | ;`wip.teeshirt-orders/Webview-HFQL wip.teeshirt-orders/Webview-HFQL
85 | ; `wip.demo-branched-route/RecursiveRouter wip.demo-branched-route/RecursiveRouter
86 | ; `wip.demo-explorer2/DirectoryExplorer-HFQL wip.demo-explorer2/DirectoryExplorer-HFQL
87 |
88 | ; Hyperfiddle tutorials
89 | ; `wip.tag-picker/TagPicker wip.tag-picker/TagPicker
90 |
91 | ; Triage
92 | ; `user.demo-virtual-scroll/VirtualScroll user.demo-virtual-scroll/VirtualScroll
93 | ; `user.demo-tic-tac-toe/TicTacToe user.demo-tic-tac-toe/TicTacToe
94 |
95 | ; Tests
96 | ; ::demos/dennis-exception-leak wip.dennis-exception-leak/App2
97 |
98 | ;`wip.js-interop/QRCode wip.js-interop/QRCode
99 | })
100 |
101 | #?(:clj (defn resolve-var-or-ns [sym]
102 | (if (qualified-symbol? sym)
103 | (ns-resolve *ns* sym)
104 | (the-ns sym))))
105 |
106 | #?(:clj (defn get-src [sym]
107 | (try (-> (resolve-var-or-ns sym) meta :file
108 | (->> (str "src/")) slurp)
109 | (catch java.io.FileNotFoundException _))))
110 |
111 | #?(:clj (defn get-readme [sym]
112 | (try (-> (resolve-var-or-ns sym) meta :file
113 | (clojure.string/split #"\.(clj|cljs|cljc)") first (str ".md")
114 | (->> (str "src/")) slurp)
115 | (catch java.io.FileNotFoundException _))))
116 |
117 | (comment
118 | (get-src `user.demo-two-clocks/TwoClocks)
119 | (get-src 'user)
120 | (get-readme 'user)
121 | (-> (resolve-var-or-ns 'user) meta :file)
122 | (get-readme `user.demo-two-clocks/TwoClocks))
123 |
124 | (e/defn Code [page]
125 | (e/client
126 | (dom/fieldset
127 | (dom/props {:class "user-examples-code"})
128 | (dom/legend (dom/text "Code"))
129 | #_(dom/pre (dom/text (e/server (get-src page))))
130 | (CodeMirror. {:parent dom/node :readonly true} identity identity (e/server (get-src page))))))
131 |
132 | (e/defn App [page]
133 | (e/client
134 | (dom/fieldset
135 | (dom/props {:class ["user-examples-target" (some-> page name)]})
136 | (dom/legend (dom/text "Result"))
137 | (e/server (new (get pages page NotFoundPage))))))
138 |
139 | (e/defn Markdown [?md-str]
140 | (e/client
141 | (let [html (e/server (some-> ?md-str markdown.core/md-to-html-string))]
142 | (set! (.-innerHTML dom/node) html))))
143 |
144 | (e/defn Readme [page]
145 | (e/client
146 | (dom/div
147 | (dom/props {:class "user-examples-readme markdown-body"})
148 | (e/server (Markdown. (get-readme page))))))
149 |
150 | (def tutorials
151 | [["Electric"
152 | [{::id `user.demo-two-clocks/TwoClocks
153 | ::title "Two Clocks – Hello World"
154 | ::lead "Streaming lexical scope. The server clock is streamed to the client."}
155 | {::id `user.demo-toggle/Toggle
156 | ::lead "This demo toggles between client and server with a button."}
157 | {::id `user.demo-system-properties/SystemProperties
158 | ::lead "A larger example of a HTML table backed by a server-side query. Type into the input and see the query update live."}
159 | {::id `user.demo-chat/Chat ::lead "A multiplayer chat app in 30 LOC, all one file. Try two tabs."}
160 | {::id `user.tutorial-backpressure/Backpressure ::lead "This is just the Two Clocks demo with slight modifications, there is more to learn here."}
161 | {::id `user.tutorial-lifecycle/Lifecycle ::title "Component Lifecycle" ::lead "mount/unmount component lifecycle"}
162 | #_{::id 'user ::title "Electric Entrypoint" ::suppress-demo true
163 | ::lead "This is the Electric entrypoint (in user.cljs). `hyperfiddle.electric/boot` is the Electric compiler entrypoint."}
164 | {::id `user.demo-chat-extended/ChatExtended
165 | ::lead "Extended chat demo with auth and presence. When multiple sessions are connected, you can see who else is present."}
166 | {::id `user.demo-webview/Webview
167 | ::lead "A database backed webview with reactive updates."}
168 | {::id `user.demo-todos-simple/TodoList
169 | ::lead "minimal todo list. it's multiplayer, try two tabs"}
170 | {::id `user.demo-reagent-interop/ReagentInterop
171 | ::lead "Reagent (React.js) embedded inside Electric. The reactive mouse coordinates cross from Electric to Reagent via props."}
172 | {::id `user.demo-svg/SVG
173 | ::lead "SVG support. Note the animation is reactive and driven by javascript cosine."}
174 |
175 | ; 7 GUIs
176 | {::id `user.tutorial-7guis-1-counter/Counter ::title "7GUIs Counter"
177 | ::lead "See "}
178 | {::id `user.tutorial-7guis-2-temperature/TemperatureConverter ::title "7GUIs Temperature Converter"
179 | ::lead "See "}
180 | {::id `user.tutorial-7guis-4-timer/Timer ::title "7GUIs Timer"
181 | ::lead "See "}
182 | {::id `user.tutorial-7guis-5-crud/CRUD ::title "7GUIs CRUD"
183 | ::lead "See "}
184 |
185 | ; Demos
186 | #_{::id `user.demo-todomvc/TodoMVC ::suppress-code true ::lead "TodoMVC as a function"}
187 | #_{::id `user.demo-todomvc-composed/TodoMVC-composed ::suppress-code true ::lead "Demo of app composition by putting a whole fullstack app inside a for loop."}
188 | #_{::id `user.demo-explorer/DirectoryExplorer ::suppress-code true ::lead "Server-streamed virtual pagination over node_modules. Check the DOM!"}
189 | ;; #_{::id `wip.demo-stage-ui4/CrudForm ::lead "Database-backed CRUD form using Datomic"}
190 | ;; {::id `wip.demo-custom-types/CustomTypes ::lead "Custom transit serializers example"}
191 | #_{::id `wip.js-interop/QRCode ::lead "Generate QRCodes with a lazily loaded JS library"}
192 | ]]
193 | #_["HFQL"
194 | [{::id `wip.teeshirt-orders/Webview-HFQL
195 | ::lead "HFQL hello world. HFQL is a data notation for CRUD apps."}]]])
196 |
197 | (def tutorials-index (->> tutorials
198 | (mapcat (fn [[_group entries]] entries))
199 | (map-indexed (fn [idx entry] (assoc entry ::order idx)))
200 | (contrib.data/index-by ::id)))
201 | (def tutorials-seq (vec (sort-by ::order (vals tutorials-index))))
202 |
203 | (defn get-prev-next [page]
204 | (when-let [order (::order (tutorials-index page))]
205 | [(get tutorials-seq (dec order))
206 | (get tutorials-seq (inc order))]))
207 |
208 | (defn title [{:keys [::id ::title]}] (or title (name id)))
209 |
210 | (e/defn Nav [page footer?]
211 | (e/client
212 | (let [[prev next] (get-prev-next page)]
213 | (dom/div {} (dom/props {:class [(if footer? "user-examples-footer-nav" "user-examples-nav")
214 | (when-not prev "user-examples-nav-start")
215 | (when-not next "user-examples-nav-end")]})
216 | (when prev
217 | (history/link [(::id prev)] (dom/props {:class "user-examples-nav-prev"}) (dom/text (str "< " (title prev)))))
218 | (dom/div (dom/props {:class "user-examples-select"})
219 | (svg/svg (dom/props {:viewBox "0 0 20 20"})
220 | (svg/path (dom/props {:d "M19 4a1 1 0 01-1 1H2a1 1 0 010-2h16a1 1 0 011 1zm0 6a1 1 0 01-1 1H2a1 1 0 110-2h16a1 1 0 011 1zm-1 7a1 1 0 100-2H2a1 1 0 100 2h16z"})))
221 | (dom/select
222 | (e/for [[group-label entries] tutorials]
223 | (dom/optgroup (dom/props {:label group-label})
224 | (e/for [{:keys [::id]} entries]
225 | (let [entry (tutorials-index id)]
226 | (dom/option
227 | (dom/props {:value (str id) :selected (= page id)})
228 | (dom/text (str (inc (::order entry)) ". " (title entry))))))))
229 | (dom/on "change" (e/fn [^js e]
230 | (history/navigate! history/!history [(clojure.edn/read-string (.. e -target -value))])))))
231 | (when next
232 | (history/link [(::id next)] (dom/props {:class "user-examples-nav-next"}) (dom/text (str (title next) " >"))))))))
233 |
234 | (e/defn Examples []
235 | (e/client
236 | (let [[page & [?panel]] history/route
237 | suppress-code? (::suppress-code (get tutorials-index page))
238 | suppress-demo? (::suppress-demo (get tutorials-index page))]
239 | (case ?panel
240 | code (Code. page) ; iframe url for just code
241 | app (history/router 1 (App. page)) ; iframe url for just app
242 | (do
243 | (when suppress-code?
244 | (.. dom/node -classList (add "user-examples-demo"))
245 | (e/on-unmount #(.. dom/node -classList (remove "user-examples-demo"))))
246 | (dom/h1 (dom/text "Tutorial – Electric Clojure"))
247 | (Nav. page false)
248 | (dom/div (dom/props {:class "user-examples-lead"})
249 | (e/server (Markdown. (::lead (get tutorials-index page)))))
250 | (when-not suppress-demo?
251 | (history/router 1 ; focus route slot 1 to store state: `[page ]
252 | (App. page)))
253 | (when-not suppress-code?
254 | (Code. page))
255 | (Readme. page)
256 | (Nav. page true))))))
257 |
258 | (defn route->path [route] (clojure.string/join "/" (map contrib.ednish/encode-uri route)))
259 | (defn path->route [s]
260 | (let [s (contrib.ednish/discard-leading-slash s)]
261 | (case s "" nil (->> (clojure.string/split s #"/") (mapv contrib.ednish/decode-uri)))))
262 |
263 | ; nested routes don't work yet, check dir explorer etc
264 |
265 | (comment
266 | (clojure.string/split "/user.demo-two-clocks!TwoClocks" #"/")
267 | (clojure.string/split "user.demo-two-clocks!TwoClocks" #"/")
268 | (clojure.string/split "" #"/")
269 | (route->path [`user.demo-two-clocks/TwoClocks]) := "user.demo-two-clocks!TwoClocks"
270 | (path->route "user.demo-two-clocks!TwoClocks") := [`user.demo-two-clocks/TwoClocks]
271 | (path->route "/user.demo-two-clocks!TwoClocks") := [`user.demo-two-clocks/TwoClocks]
272 | (path->route "/user.demo-two-clocks!TwoClocks/") := [`user.demo-two-clocks/TwoClocks]
273 | (path->route "/user.demo-two-clocks!TwoClocks/foo") := [`user.demo-two-clocks/TwoClocks 'foo]
274 | (path->route "") := nil
275 | )
276 |
277 | (e/defn Main [ring-req]
278 | (binding [e/http-request ring-req]
279 | (e/client
280 | (binding [history/encode route->path
281 | history/decode #(or (path->route %) [`user.demo-two-clocks/TwoClocks])]
282 | (history/router (history/HTML5-History.)
283 | (set! (.-title js/document) (str #_(clojure.string/capitalize) (some-> (identity history/route) first name) " – Electric Clojure"))
284 | (binding [dom/node js/document.body]
285 | (Examples.)))))))
286 |
--------------------------------------------------------------------------------
/src/wip/demo_branched_route.cljc:
--------------------------------------------------------------------------------
1 | (ns wip.demo-branched-route
2 | (:require datascript.core
3 | #?(:clj user.example-datascript-db)
4 | [hyperfiddle.api :as hf]
5 | [hyperfiddle.hfql-tree-grid :as ttgui]
6 | [hyperfiddle.electric :as e]
7 | [hyperfiddle.electric-dom2 :as dom]
8 | hyperfiddle.popover
9 | [hyperfiddle.history :as router]
10 | wip.orders-datascript))
11 |
12 | (e/def Page)
13 | (e/defn Page-impl []
14 | (dom/h1 (dom/text "Branched route"))
15 | (dom/pre (dom/text (contrib.str/pprint-str router/route)))
16 |
17 | (router/router :hfql
18 | (e/server
19 | (binding [hf/*nav!* wip.orders-datascript/nav!
20 | hf/*schema* wip.orders-datascript/schema
21 | hf/db hf/*$*]
22 | (ttgui/with-gridsheet-renderer
23 | (binding [ttgui/grid-width 2
24 | hf/db-name "$"]
25 | (e/server
26 | (hf/hfql {(wip.orders-datascript/orders .) [:db/id]})))))))
27 |
28 | (dom/hr)
29 | (router/router :left (hyperfiddle.popover/popover "Recur Left" (Page.)))
30 | (router/router :right (hyperfiddle.popover/popover "Recur Right" (Page.))))
31 |
32 | (e/defn RecursiveRouter []
33 | (hf/branch
34 | (e/client
35 | (binding [Page Page-impl]
36 | (router/router 1 ; ordinal to nominal - representation only
37 | (Page.))))))
38 |
39 | (comment
40 | (e/for [[page nested] s]
41 | (router/router page (hyperfiddle.popover/popover "Recur Left"
42 | (hf/eval-as-iframe nested))))
43 |
44 | `(wip.demo-branched-route/RecursiveRouter
45 | {::needle "root"
46 | ::left {::needle ""
47 | ::left {::needle ""
48 | ::right {}}}
49 | ::right {::needle ""}})
50 |
51 | `(wip.demo-branched-route/RecursiveRouter
52 | {::left `(wip.demo-branched-route/PDF)
53 | ::right `(wip.demo-branched-route/HTML)})
54 | )
--------------------------------------------------------------------------------
/src/wip/demo_custom_types.cljc:
--------------------------------------------------------------------------------
1 | (ns wip.demo-custom-types
2 | (:require [hyperfiddle.electric :as e]
3 | [hyperfiddle.electric-dom2 :as dom]
4 | [cognitect.transit :as t]))
5 |
6 | (defrecord MyCustomType [field]) ; custom type
7 |
8 | (def write-handler
9 | (t/write-handler
10 | (fn [_] "wip.demo-custom-types/MyCustomType") ; this must be namespaced!
11 | (fn [x] (into {} x))))
12 |
13 | (def read-handler (t/read-handler map->MyCustomType))
14 |
15 | ; Todo refactor and cleanup, there are better ways to do this
16 | #?(:clj (alter-var-root #'hyperfiddle.electric.impl.io/*write-handlers*
17 | assoc MyCustomType write-handler)) ; server: write only
18 | #?(:cljs (set! hyperfiddle.electric.impl.io/*read-handlers* ; client: read only
19 | (assoc hyperfiddle.electric.impl.io/*read-handlers*
20 | "wip.demo-custom-types/MyCustomType" read-handler)))
21 |
22 | (e/defn CustomTypes []
23 | (e/server
24 | (let [object (MyCustomType. "value")]
25 | (e/client
26 | (dom/dl
27 | (dom/dt (dom/text "type")) (dom/dd (dom/text (pr-str (type object))))
28 | (dom/dt (dom/text "value")) (dom/dd (dom/text (pr-str object))))))))
29 |
--------------------------------------------------------------------------------
/src/wip/demo_explorer2.cljc:
--------------------------------------------------------------------------------
1 | (ns wip.demo-explorer2
2 | (:require [clojure.datafy :refer [datafy]]
3 | [clojure.core.protocols :refer [nav]]
4 | #?(:clj clojure.java.io)
5 | [clojure.spec.alpha :as s]
6 | [contrib.datafy-fs #?(:clj :as :cljs :as-alias) fs]
7 | [hyperfiddle.api :as hf]
8 | [hyperfiddle.electric :as e]
9 | [hyperfiddle.electric-dom2 :as dom]
10 | [hyperfiddle.history :as router]
11 | [hyperfiddle.hfql-tree-grid :as ttgui]))
12 |
13 | (def unicode-folder "\uD83D\uDCC2") ; 📂
14 |
15 | (e/defn DirectoryExplorer-HFQL []
16 | (ttgui/with-gridsheet-renderer
17 | (binding [hf/db-name "$"]
18 | (dom/style {:grid-template-columns "repeat(5, 1fr)"})
19 | (e/server
20 | (binding [hf/*nav!* (fn [db e a] (a (datafy e))) ;; FIXME db is specific, hfql should be general
21 | hf/*schema* (constantly nil)] ;; FIXME this is datomic specific, hfql should be general
22 | (let [path (fs/absolute-path "node_modules")]
23 | (hf/hfql {(props (fs/list-files (props path {::dom/disabled true})) ;; FIXME forward props
24 | {::hf/height 30})
25 | [(props ::fs/name #_{::hf/render (e/fn [{::hf/keys [Value]}]
26 | (let [v (Value.)]
27 | (case (::fs/kind m)
28 | ::fs/dir (let [absolute-path (::fs/absolute-path m)]
29 | (e/client (router/Link. [::fs/dir absolute-path] v)))
30 | (::fs/other ::fs/symlink ::fs/unknown-kind) v
31 | v #_(e/client (router/Link. [::fs/file x] v)))))})
32 |
33 | ;; TODO add links and indentation
34 |
35 | (props ::fs/modified {::hf/render (e/fn [{::hf/keys [Value]}]
36 | (e/client
37 | (dom/text
38 | (-> (e/server (Value.))
39 | .toISOString
40 | (.substring 0 10)))))})
41 | ::fs/size
42 | (props ::fs/kind {::hf/render (e/fn [{::hf/keys [Value]}]
43 | (let [v (Value.)]
44 | (e/client
45 | (case v
46 | ::fs/dir (dom/text unicode-folder)
47 | (dom/text (some-> v name))))))})
48 | ]})))))))
49 |
--------------------------------------------------------------------------------
/src/wip/demo_stage_ui4.cljc:
--------------------------------------------------------------------------------
1 | (ns wip.demo-stage-ui4
2 | (:require [contrib.css :refer [css-slugify]]
3 | [contrib.str :refer [pprint-str]]
4 | #?(:clj [contrib.datomic-contrib :as dx])
5 | #?(:clj [datomic.client.api :as d])
6 | [hyperfiddle.api :as hf]
7 | [hyperfiddle.electric :as e]
8 | [hyperfiddle.electric-dom2 :as dom]
9 | [hyperfiddle.electric-ui4 :as ui]
10 | [hyperfiddle.popover :refer [Popover]]))
11 |
12 | (def cobblestone 536561674378709)
13 |
14 | (def label-form-spec
15 | [:db/id
16 | :label/gid
17 | :label/name
18 | :label/sortName
19 | {:label/type [:db/ident]}
20 | {:label/country [:db/ident]}
21 | :label/startYear])
22 |
23 | (comment (d/pull test/datomic-db ['*] cobblestone))
24 |
25 | #?(:clj (defn type-options [db & [needle]]
26 | (->> (d/q '[:find (pull ?e [:db/ident]) :in $ ?needle :where
27 | [?e :db/ident ?ident]
28 | [(namespace ?ident) ?x-ns] [(= ?x-ns "label.type")]
29 | [(name ?ident) ?x-label]
30 | [(contrib.str/includes-str? ?x-label ?needle)]]
31 | db (or needle ""))
32 | (map first))))
33 |
34 | (comment
35 | (type-options test/datomic-db "")
36 | (type-options test/datomic-db "prod")
37 | (type-options test/datomic-db "bootleg")
38 | (type-options test/datomic-db nil))
39 |
40 |
41 | (e/defn Form [e]
42 | (e/server
43 | (let [record (d/pull hf/db label-form-spec e)]
44 | (e/client
45 | (dom/dl
46 |
47 | (dom/dt (dom/text "id"))
48 | (dom/dd
49 | (ui/input (:db/id record) nil
50 | (dom/props {::dom/disabled true})))
51 |
52 | (dom/dt "gid")
53 | (dom/dd (ui/uuid (:label/gid record) nil
54 | (dom/props {::dom/disabled true})))
55 |
56 | (dom/dt (dom/text "name"))
57 | (dom/dd (ui/input (:label/name record)
58 | (e/fn [v]
59 | (println 'input! v)
60 | (e/server
61 | (hf/Transact!. [[:db/add e :label/name v]])))))
62 |
63 | (dom/dt (dom/text "sortName"))
64 | (dom/dd (ui/input (:label/sortName record)
65 | (e/fn [v]
66 | (e/server
67 | (hf/Transact!. [[:db/add e :label/sortName v]])))))
68 |
69 |
70 | (dom/dt (dom/text "type"))
71 | (dom/dd
72 | (e/server
73 | (ui/typeahead
74 | (:label/type record)
75 | (e/fn V! [option]
76 | (hf/Transact!. [[:db/add e :label/type (:db/ident option)]]))
77 | (e/fn Options [search] (type-options hf/db search))
78 | (e/fn OptionLabel [option] (some-> option :db/ident name)))))
79 |
80 | ; country
81 |
82 | (dom/dt (dom/text "startYear"))
83 | (dom/dd (ui/long
84 | (:label/startYear record)
85 | (e/fn [v]
86 | (e/server
87 | (hf/Transact!. [[:db/add e :label/startYear v]]))))))
88 |
89 | (dom/pre (dom/text (pprint-str record)))))))
90 |
91 | (e/defn Page []
92 | #_(e/client (dom/div (if hf/loading "loading" "idle") " "
93 | (str (hf/Load-timer.)) "ms"))
94 | (Form. cobblestone)
95 | #_(Form. cobblestone)
96 | (e/client (Popover. "open" (e/fn [] (e/server (Form. cobblestone))))))
97 |
98 | (e/defn CrudForm []
99 | (e/server
100 | (let [conn @(requiring-resolve 'test/datomic-conn)
101 | secure-db (d/with-db conn)] ; todo datomic-tx-listener
102 | (binding [hf/schema (new (dx/schema> secure-db))
103 | hf/into-tx' hf/into-tx
104 | hf/with (fn [db tx] ; inject datomic
105 | (try (:db-after (d/with db {:tx-data tx}))
106 | (catch Exception e
107 | (println "...failure, e: " e)
108 | db)))
109 | hf/db secure-db]
110 | (hf/branch
111 | (Page.)
112 | (e/client
113 | (dom/hr)
114 | (ui/edn
115 | (e/server hf/stage) nil
116 | (dom/props {:disabled true :class (css-slugify `staged)}))))))))
117 |
--------------------------------------------------------------------------------
/src/wip/js_interop.cljc:
--------------------------------------------------------------------------------
1 | (ns wip.js-interop
2 | (:require [hyperfiddle.electric :as e]
3 | [hyperfiddle.electric-dom2 :as dom]
4 | [hyperfiddle.electric-ui4 :as ui]
5 | [hyperfiddle.history :as history]))
6 |
7 | (e/defn QRCodeApp []
8 | (e/client
9 | (let [value (or (::value history/route) (str js/window.location.origin))
10 | !ready (atom false)]
11 | (dom/script
12 | (dom/props {:src "https://cdn.rawgit.com/davidshimjs/qrcodejs/gh-pages/qrcode.min.js"})
13 | (dom/on! "load" (fn [_] (reset! !ready true))))
14 | (if-not (e/watch !ready)
15 | (dom/p (dom/text "Loading..."))
16 | (do
17 | (ui/input value (e/fn [value] (history/swap-route! assoc ::value value)))
18 | (dom/div
19 | (let [^js qrcode (js/QRCode. dom/node (clj->js {:width 128
20 | :height 128
21 | :colorDark "#000000"
22 | :colorLight "#ffffff"
23 | :correctLevel js/QRCode.CorrectLevel.H}))]
24 | ((fn [value]
25 | (.clear qrcode)
26 | (.makeCode qrcode value))
27 | value))))))))
28 |
--------------------------------------------------------------------------------
/src/wip/orders_datascript.cljc:
--------------------------------------------------------------------------------
1 | (ns wip.orders-datascript
2 | "query functions used in tee-shirt orders demo"
3 | (:require [clojure.spec.alpha :as s]
4 | clojure.string
5 | contrib.str
6 | [datascript.core :as d]
7 | [hyperfiddle.api :as hf]
8 | [hyperfiddle.rcf :refer [tap % tests]]))
9 |
10 | (s/fdef genders :args (s/cat) :ret (s/coll-of number?))
11 | (defn genders []
12 | (into [] (sort (d/q '[:find [?e ...] :where [_ :order/gender ?e]] hf/*$*))))
13 |
14 | (s/fdef shirt-sizes :args (s/cat :gender keyword?
15 | :needle string?)
16 | :ret (s/coll-of number?))
17 |
18 | (defn shirt-sizes [gender needle]
19 | ; resolve db/id and db/ident genders to same entity
20 | ; datomic does this transparently
21 | ; datascript does not
22 | (sort
23 | (if gender
24 | (d/q '[:in $ ?gender ?needle
25 | :find [?e ...]
26 | :where
27 | [?e :order/type :order/shirt-size]
28 | [?e :order/gender ?g]
29 | [?g :db/ident ?gender]
30 | [?e :db/ident ?ident] ; remove
31 | [(name ?ident) ?nm]
32 | [(contrib.str/includes-str? ?nm ?needle)]]
33 | hf/*$*
34 | gender (or needle ""))
35 | (d/q '[:in $ ?needle
36 | :find [?e ...]
37 | :where
38 | [?e :order/type :order/shirt-size]
39 | [?e :db/ident ?ident]
40 | [(name ?ident) ?nm]
41 | [(contrib.str/includes-str? ?nm ?needle)]]
42 | hf/*$*
43 | (or needle "")))))
44 |
45 | (tests
46 | (shirt-sizes :order/female #_2 "") := [6 7 8]
47 | (shirt-sizes :order/female #_2 "med") := [7]
48 | (shirt-sizes :order/female #_2 "d") := [7])
49 |
50 | (defn orders [needle]
51 | (sort (d/q '[:find [?e ...] :in $ ?needle :where
52 | [?e :order/email ?email]
53 | [(clojure.string/includes? ?email ?needle)]]
54 | hf/*$* (or needle ""))))
55 |
56 | (tests
57 | (orders "") := [9 10 11]
58 | (orders "example") := [9 10 11]
59 | (orders "b") := [10])
60 |
61 | (s/fdef orders :args (s/cat :needle string?)
62 | :ret (s/coll-of (s/keys :req [:order/email
63 | :order/email1
64 | :order/gender
65 | :order/shirt-size])))
66 |
67 | (s/fdef order :args (s/cat :needle string?) :ret number?)
68 | (defn order [needle] (first (orders needle)))
69 |
70 | (tests
71 | (order "") := 9
72 | (order "bob") := 10)
73 |
74 | (s/fdef one-order :args (s/cat :sub any?) :ret any?)
75 | (defn one-order [sub] (hf/*nav!* hf/*$* sub :db/id))
76 |
77 |
--------------------------------------------------------------------------------
/src/wip/orders_datomic.clj:
--------------------------------------------------------------------------------
1 | (ns wip.orders-datomic
2 | "query functions used in tee-shirt orders demo"
3 | (:require [clojure.spec.alpha :as s]
4 | contrib.str
5 | [hyperfiddle.api :as hf]
6 | [hyperfiddle.rcf :refer [tap % tests]]))
7 |
8 | (try (require '[datomic.api :as d])
9 | (catch java.io.FileNotFoundException e
10 | (throw (ex-info "datomic.api not available, check Datomic pro version >= 1.0.6527" {}))))
11 |
12 | (defn fixtures [$]
13 | ; portable
14 | (-> $
15 | (d/with [{:db/ident :order/male}
16 | {:db/ident :order/female}])
17 | :db-after
18 | (d/with [{:order/type :order/shirt-size :db/ident :order/mens-small :order/gender :order/male}
19 | {:order/type :order/shirt-size :db/ident :order/mens-medium :order/gender :order/male}
20 | {:order/type :order/shirt-size :db/ident :order/mens-large :order/gender :order/male}
21 | {:order/type :order/shirt-size :db/ident :order/womens-small :order/gender :order/female}
22 | {:order/type :order/shirt-size :db/ident :order/womens-medium :order/gender :order/female}
23 | {:order/type :order/shirt-size :db/ident :order/womens-large :order/gender :order/female}])
24 | :db-after
25 | (d/with [{:order/email "alice@example.com" :order/gender :order/female :order/shirt-size :order/womens-large
26 | :order/tags [:a :b :c]}
27 | {:order/email "bob@example.com" :order/gender :order/male :order/shirt-size :order/mens-large
28 | :order/tags [:b]}
29 | {:order/email "charlie@example.com" :order/gender :order/male :order/shirt-size :order/mens-medium}])
30 | :db-after
31 | #_(d/with [{:db/id 12 :order/email "alice@example.com" :order/gender :order/female :order/shirt-size :order/womens-large}
32 | {:order/email "bob@example.com" :order/gender :order/male :order/shirt-size :order/mens-large}
33 | {:order/email "charlie@example.com" :order/gender :order/male :order/shirt-size :order/mens-medium}])
34 |
35 | #_:db-after))
36 |
37 | (defn init-datomic []
38 | (let [schema [{:db/ident :order/email :db/valueType :db.type/string :db/cardinality :db.cardinality/one :db/unique :db.unique/identity}
39 | {:db/ident :order/gender :db/valueType :db.type/ref :db/cardinality :db.cardinality/one}
40 | {:db/ident :order/shirt-size :db/valueType :db.type/ref :db/cardinality :db.cardinality/one}
41 | {:db/ident :order/type :db/valueType :db.type/keyword :db/cardinality :db.cardinality/one}
42 | {:db/ident :order/tags :db/valueType :db.type/keyword :db/cardinality :db.cardinality/many}]]
43 | (d/create-database "datomic:mem://hello-world")
44 | (def ^:dynamic *$* (-> (d/connect "datomic:mem://hello-world") d/db (d/with schema) :db-after fixtures))))
45 |
46 |
47 | (init-datomic)
48 |
49 |
50 | (s/fdef genders :args (s/cat) :ret (s/coll-of number?))
51 | (defn genders []
52 | (into [] (sort (d/q '[:find [?ident ...] :where [_ :order/gender ?e] [?e :db/ident ?ident]] hf/*$*))))
53 |
54 | (tests
55 | (binding [hf/*$* *$*]
56 | (genders)) := [:order/female :order/male])
57 |
58 | (s/fdef shirt-sizes :args (s/cat :gender keyword?
59 | :needle string?)
60 | :ret (s/coll-of number?))
61 |
62 | (defn shirt-sizes [gender needle]
63 | ;; resolve db/id and db/ident genders to same entity datomic does this
64 | ;; transparently datascript does not
65 | (sort
66 | (if gender
67 | (d/q '[:in $ ?gender ?needle
68 | :find [?ident ...]
69 | :where
70 | [?e :order/type :order/shirt-size]
71 | [?e :order/gender ?g]
72 | [?g :db/ident ?gender]
73 | [?e :db/ident ?ident] ; remove
74 | [(contrib.str/includes-str? ?ident ?needle)]]
75 | hf/*$*
76 | gender (or needle ""))
77 | (d/q '[:in $ ?needle
78 | :find [?e ...]
79 | :where
80 | [?e :order/type :order/shirt-size]
81 | [?e :db/ident ?ident]
82 | [(contrib.str/includes-str? ?ident ?needle)]]
83 | hf/*$*
84 | (or needle "")))))
85 |
86 | (tests
87 | (binding [hf/*$* *$*]
88 | (shirt-sizes :order/female #_2 "") := [:order/womens-large :order/womens-medium :order/womens-small]
89 | (shirt-sizes :order/female #_2 "med") := [:order/womens-medium]))
90 |
91 | (defn orders [needle]
92 | (sort (d/q '[:find [?e ...] :in $ ?needle :where
93 | [?e :order/email ?email]
94 | [(clojure.string/includes? ?email ?needle)]]
95 | hf/*$* (or needle ""))))
96 |
97 | (tests
98 | (binding [hf/*$* *$*]
99 | (orders "") := [17592186045428 17592186045429 17592186045430]
100 | (orders "example") := [17592186045428 17592186045429 17592186045430]
101 | (orders "b") := [17592186045429]))
102 |
103 | (s/fdef orders :args (s/cat :needle string?)
104 | :ret (s/coll-of (s/keys :req [:order/email
105 | :order/email1
106 | :order/gender
107 | :order/shirt-size])))
108 |
109 | (s/fdef order :args (s/cat :needle string?) :ret number?)
110 | (defn order [needle] (first (orders needle)))
111 |
112 | (tests
113 | (binding [hf/*$* *$*]
114 | (order "") := 17592186045428
115 | (order "bob") := 17592186045429))
116 |
117 | (s/fdef one-order :args (s/cat :sub any?) :ret any?)
118 | (defn one-order [sub] (hf/*nav!* hf/*$* sub :db/id))
119 |
120 |
121 | (defn nav! [db e a] (let [v (get (d/entity db e) a)]
122 | (prn "nav! datiomic - " e a v)
123 | v) )
124 |
125 | (defn schema [db a] (when (qualified-keyword? a) (d/entity db a)))
126 |
--------------------------------------------------------------------------------
/src/wip/tag_picker.cljc:
--------------------------------------------------------------------------------
1 | (ns wip.tag-picker
2 | (:require [clojure.string :as str]
3 | [hyperfiddle.electric :as e]
4 | [hyperfiddle.electric-ui4 :as ui]))
5 |
6 | (def data {:alice {:name "Alice B"}
7 | :bob {:name "Bob C"}
8 | :charlie {:name "Charlie D"}
9 | :derek {:name "Derek E"}})
10 | (defn q [search] (into [] (keep (fn [[k {nm :name}]] (when (str/includes? nm search) k))) data))
11 |
12 | (e/defn TagPicker []
13 | (e/server
14 | (let [!v (atom #{:alice :bob})]
15 | (ui/tag-picker (e/watch !v)
16 | (e/fn [v] (e/client (prn [:V! v])) (swap! !v conj v))
17 | (e/fn [v] (prn [:unV! v]) (swap! !v disj v))
18 | (e/fn [search] (e/client (prn [:Options search])) (q search))
19 | (e/fn [id] (e/client (prn [:OptionLabel id])) (-> data id :name))))))
20 |
--------------------------------------------------------------------------------
/src/wip/teeshirt_orders.cljc:
--------------------------------------------------------------------------------
1 | (ns wip.teeshirt-orders
2 | (:require [clojure.spec.alpha :as s]
3 | #?(:clj [datascript.core :as d])
4 | #?(:clj [datascript.impl.entity :refer [entity?]])
5 | [hyperfiddle.api :as hf]
6 | [hyperfiddle.hfql :refer [hfql]]
7 | [hyperfiddle.hfql-tree-grid :as hfql-tree-grid]
8 | [hyperfiddle.electric :as e]))
9 |
10 | (declare conn schema nav orders)
11 | (def ^:dynamic db)
12 |
13 | (e/defn Teeshirt-orders []
14 | (hfql [db hf/db] ; convey reactive db to clojure dynamic
15 | {(orders .)
16 | [:db/id
17 | :order/email
18 | :order/gender]}))
19 |
20 | (s/fdef orders :args (s/cat :needle string?) :ret (s/coll-of any?))
21 | (defn orders [needle]
22 | #?(:clj
23 | (sort (d/q '[:find [?e ...] :in $ ?needle :where
24 | [?e :order/email ?email]
25 | [(clojure.string/includes? ?email ?needle)]]
26 | db (or needle "")))))
27 |
28 | (e/defn Webview-HFQL []
29 | (e/client
30 | (hfql-tree-grid/with-gridsheet-renderer
31 | (e/server
32 | (binding [hf/db (e/watch conn)
33 | hf/*schema* schema
34 | hf/*nav!* nav]
35 | (Teeshirt-orders.))))))
36 |
37 | #?(:clj (defn schema [db a] (get-in db [:schema a])))
38 | #?(:clj (defn nav [db e a]
39 | (let [v (a (d/entity db e))]
40 | (if (entity? v) (or (:db/ident v) (:db/id v)) v))))
41 |
42 | #?(:clj
43 | (defonce conn
44 | (doto (d/create-conn {})
45 | (d/transact! ; test data
46 | [{:order/email "alice@example.com" :order/gender :order/female}
47 | {:order/email "bob@example.com" :order/gender :order/male}
48 | {:order/email "charlie@example.com" :order/gender :order/male}]))))
--------------------------------------------------------------------------------
/src/wip/teeshirt_orders.md:
--------------------------------------------------------------------------------
1 | What's happening
2 | * there's a CRUD table, backed by a query
3 | * the table is specified by 4 lines of HFQL + database schema + the clojure.spec for the query
4 | * 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`
5 | * type "alice" into the input and see the query refresh live
6 |
7 | Novel forms
8 | * `hf/hfql`
9 | * `binding`: Electric dynamic scope, it's reactive and used for dependency injection
10 | * `hf/db`
11 | * `hf/*schema*`
12 | * `hf/*nav!*`
13 |
14 | Key ideas
15 | * HFQL's mission is to let you model CRUD apps in as few LOC as possible.
16 | * HFQL generalizes graph-pull query notation into a declarative UI specification language.
17 | * spec-driven UI
18 | * macroexpands down to Electric
19 | * network-transparent
20 | * composes with Electric as e/defn
21 | * scope
22 |
23 | Dependency injection with Electric bindings
--------------------------------------------------------------------------------
/src/wip/tracing.cljc:
--------------------------------------------------------------------------------
1 | (ns wip.tracing
2 | (:require [hyperfiddle.electric :as e]
3 | [hyperfiddle.electric-dom2 :as dom]
4 | [contrib.trace :as ct]
5 | [contrib.trace.datascript-tracer :as ds-tracer]
6 | [hyperfiddle.electric-ui4 :as ui]
7 | [clojure.string :as str]))
8 |
9 | (e/defn FizzBuzzText [n]
10 | (ct/trace :text
11 | (ct/trace :n n)
12 | (if-some [strs (ct/trace :strs
13 | (transduce (filter identity) conj nil
14 | [(when (zero? (mod n 5)) "Buzz")
15 | (when (zero? (mod n 3)) "Fizz")]))]
16 | (str/join "" strs)
17 | (str n))))
18 |
19 | (e/defn CodeFor [sym height]
20 | (e/client
21 | (ui/edn (e/server (nth (:hyperfiddle.electric.impl.compiler/node (meta (eval `(var ~sym)))) 3))
22 | (e/fn [_])
23 | (dom/props {:disabled true})
24 | (dom/style {:display "block" :width "50em", :height height}))))
25 |
26 | (e/defn FizzBuzz []
27 | (e/client
28 | (dom/div
29 | (dom/h2 (dom/text "an over-engineered fizzbuzz"))
30 | (CodeFor. `FizzBuzzText "22em")
31 | (dom/br)
32 | (let [!n (atom 0), n (e/watch !n)]
33 | (ui/long n (e/fn [v] (reset! !n v)) (dom/style {:width "8em"}))
34 | (dom/span (dom/text (FizzBuzzText. n)))))))
35 |
36 | (e/defn ExceptionOrValue [n]
37 | (ct/trace :result
38 | [(ct/trace :throw-on-even (if (even? n) (throw (ex-info "even!" {:n n})) n))
39 | (ct/trace :triple (* n 3))]))
40 |
41 | (e/defn Exceptions []
42 | (e/client
43 | (dom/div
44 | (dom/h2 (dom/text "exceptions and concurrency"))
45 | (CodeFor. `ExceptionOrValue "8em")
46 | (let [!n (atom 0), n (e/watch !n)]
47 | (ui/long n (e/fn [v] (reset! !n v)) (dom/style {:width "8em"}))
48 | (ExceptionOrValue. n)))))
49 |
50 | (e/defn Note [text] (e/client (dom/em (dom/style {:display "block"}) (dom/text text))))
51 |
52 | (e/defn TracingDemo []
53 | (e/client
54 | (ds-tracer/with-defaults
55 | (FizzBuzz.)
56 | (dom/br)
57 | (ds-tracer/DatascriptTraceView.)
58 | (dom/br)
59 | (Note. "It's like println's, but better!")
60 | (Note. "Click on 2 trace values to get the distance between them!")
61 | (Note. "If the values are too close to each other increase the time granularity."))
62 | (ds-tracer/with-defaults
63 | (Exceptions.)
64 | (dom/br)
65 | (ds-tracer/DatascriptTraceView.)
66 | (dom/br)
67 | (Note. "Note that `:triple` runs even if `:throw-on-even!` throws!")
68 | (Note. "That's because Electric code runs concurrently"))))
69 |
--------------------------------------------------------------------------------