├── example
├── todomvc
│ ├── .gitignore
│ ├── src
│ │ └── todomvc
│ │ │ ├── fx.cljs
│ │ │ ├── constant.cljs
│ │ │ ├── cofx.cljs
│ │ │ ├── router.cljs
│ │ │ ├── interceptor.cljs
│ │ │ ├── core.cljs
│ │ │ ├── sub.cljs
│ │ │ ├── event.cljs
│ │ │ └── view.cljs
│ ├── package.json
│ ├── shadow-cljs.edn
│ ├── public
│ │ ├── index.html
│ │ └── css
│ │ │ └── index.css
│ ├── README.md
│ └── deps.edn
├── test-app
│ ├── .gitignore
│ ├── package.json
│ ├── shadow-cljs.edn
│ ├── public
│ │ └── index.html
│ ├── README.md
│ ├── deps.edn
│ ├── src
│ │ └── example
│ │ │ ├── math_ml.cljs
│ │ │ ├── core.cljs
│ │ │ ├── reactive_data.cljs
│ │ │ ├── ref.cljs
│ │ │ ├── ui_component.cljs
│ │ │ ├── vcup.cljs
│ │ │ ├── context.cljs
│ │ │ ├── svg.cljs
│ │ │ └── reactive_fragment.cljs
│ └── resource
│ │ └── svg
│ │ └── fox-origami.svg
├── react-interop
│ ├── .gitignore
│ ├── shadow-cljs.edn
│ ├── public
│ │ └── index.html
│ ├── package.json
│ ├── src
│ │ └── react_interop
│ │ │ ├── core.cljs
│ │ │ └── interop.cljs
│ ├── deps.edn
│ └── README.md
└── si-frame-simple
│ ├── .gitignore
│ ├── package.json
│ ├── shadow-cljs.edn
│ ├── public
│ └── index.html
│ ├── deps.edn
│ ├── README.md
│ └── src
│ └── simple
│ └── core.cljs
├── bin
└── kaocha
├── package.json
├── playwright
├── .gitignore
├── tests
│ ├── context.spec.ts
│ ├── mathML.spec.ts
│ ├── ref.spec.ts
│ ├── svg.spec.ts
│ ├── reactiveData.spec.ts
│ ├── reactiveFragment.spec.ts
│ └── vcup.spec.ts
├── package.json
├── playwright.config.js
└── package-lock.json
├── tests.edn
├── .gitignore
├── doc
├── cljdoc.edn
├── ai-context-construction-prompt.txt
├── vcup-properties.md
└── ai-context.md
├── test
└── vrac
│ ├── test
│ └── util.cljc
│ └── dsl
│ ├── macro_test.cljc
│ └── parser_test.cljc
├── .github
└── workflows
│ ├── cljdoc.yml
│ ├── validate.yml
│ └── release.yml
├── src
└── vrac
│ ├── dsl.cljc
│ ├── dsl
│ ├── macro.cljc
│ ├── parser.cljc
│ └── ast.cljc
│ └── web.cljc
├── CHANGELOG.md
├── README.md
├── pom.xml
├── deps.edn
└── LICENSE
/example/todomvc/.gitignore:
--------------------------------------------------------------------------------
1 | public/js
2 | package-lock.json
3 |
--------------------------------------------------------------------------------
/bin/kaocha:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | clojure -M:test -m kaocha.runner "$@"
3 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "ws": "^8.18.1"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/playwright/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Playwright
3 | node_modules/
4 | /test-results/
5 | /playwright-report/
6 | /blob-report/
7 | /playwright/.cache/
8 |
--------------------------------------------------------------------------------
/tests.edn:
--------------------------------------------------------------------------------
1 | #kaocha/v1
2 | {:tests [{:id :unit
3 | :type :kaocha.type/clojure.test}
4 | {:id :unit-cljs
5 | :type :kaocha.type/cljs}]}
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | package-lock.json
3 | yarn.lock
4 | out/
5 |
6 | .idea/
7 | .cpcache/
8 | .nrepl-port
9 | .shadow-cljs/
10 | .cljs_node_repl/
11 | .clj-kondo/
12 |
13 | *.iml
14 |
--------------------------------------------------------------------------------
/example/todomvc/src/todomvc/fx.cljs:
--------------------------------------------------------------------------------
1 | (ns todomvc.fx
2 | (:require [re-frame.core :as rf]))
3 |
4 | (rf/reg-fx
5 | :save-in-local-store
6 | (fn [{:keys [key value]}]
7 | (js/localStorage.setItem key value)))
8 |
--------------------------------------------------------------------------------
/playwright/tests/context.spec.ts:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | import { test, expect } from "@playwright/test";
3 |
4 | test.beforeEach(async ({ page }) => {
5 | await page.goto("/");
6 | });
7 |
8 | test("", async ({ page }) => {});
9 |
--------------------------------------------------------------------------------
/playwright/tests/mathML.spec.ts:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | import { test, expect } from "@playwright/test";
3 |
4 | test.beforeEach(async ({ page }) => {
5 | await page.goto("/");
6 | });
7 |
8 | test("", async ({ page }) => {});
9 |
--------------------------------------------------------------------------------
/playwright/tests/ref.spec.ts:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | import { test, expect } from "@playwright/test";
3 |
4 | test.beforeEach(async ({ page }) => {
5 | await page.goto("/");
6 | });
7 |
8 | test("", async ({ page }) => {});
9 |
--------------------------------------------------------------------------------
/playwright/tests/svg.spec.ts:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | import { test, expect } from "@playwright/test";
3 |
4 | test.beforeEach(async ({ page }) => {
5 | await page.goto("/");
6 | });
7 |
8 | test("", async ({ page }) => {});
9 |
--------------------------------------------------------------------------------
/playwright/tests/reactiveData.spec.ts:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | import { test, expect } from "@playwright/test";
3 |
4 | test.beforeEach(async ({ page }) => {
5 | await page.goto("/");
6 | });
7 |
8 | test("", async ({ page }) => {});
9 |
--------------------------------------------------------------------------------
/playwright/tests/reactiveFragment.spec.ts:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | import { test, expect } from "@playwright/test";
3 |
4 | test.beforeEach(async ({ page }) => {
5 | await page.goto("/");
6 | });
7 |
8 | test("", async ({ page }) => {});
9 |
--------------------------------------------------------------------------------
/example/test-app/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | package.json.lock
3 | package-lock.json
4 | yarn.lock
5 | out/
6 | public/js/
7 |
8 | .idea/
9 | .cpcache/
10 | .nrepl-port
11 | .shadow-cljs/
12 | .cljs_node_repl/
13 |
14 | *.iml
15 |
--------------------------------------------------------------------------------
/example/react-interop/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | package.json.lock
3 | package-lock.json
4 | yarn.lock
5 | out/
6 | public/js/
7 |
8 | .idea/
9 | .cpcache/
10 | .nrepl-port
11 | .shadow-cljs/
12 | .cljs_node_repl/
13 |
14 | *.iml
15 |
--------------------------------------------------------------------------------
/example/si-frame-simple/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | package.json.lock
3 | package-lock.json
4 | yarn.lock
5 | out/
6 | public/js/
7 |
8 | .idea/
9 | .cpcache/
10 | .nrepl-port
11 | .shadow-cljs/
12 | .cljs_node_repl/
13 |
14 | *.iml
15 |
--------------------------------------------------------------------------------
/doc/cljdoc.edn:
--------------------------------------------------------------------------------
1 | {:cljdoc.doc/tree [["Readme" {:file "README.md"}]
2 | ["Changes" {:file "CHANGELOG.md"}]
3 | ["Vcup properties" {:file "doc/vcup-properties.md"}]
4 | ["AI context" {:file "doc/ai-context.md"}]]}
5 |
--------------------------------------------------------------------------------
/example/todomvc/src/todomvc/constant.cljs:
--------------------------------------------------------------------------------
1 | (ns todomvc.constant)
2 |
3 | (def enter-keycode 13)
4 | (def escape-keycode 27)
5 |
6 | (def initial-db
7 | {:next-todo-item-id 0
8 | :comp.input/title ""
9 | :todo-items {}})
10 |
11 | (def todo-item-local-storage-key "todo-items")
12 |
--------------------------------------------------------------------------------
/playwright/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "playwright",
3 | "private": true,
4 | "scripts": {
5 | "test": "npx playwright test",
6 | "test-ui": "npx playwright test --ui"
7 | },
8 | "devDependencies": {
9 | "@playwright/test": "1.55.0",
10 | "@types/node": "24.5.0"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/example/test-app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test-app",
3 | "private": true,
4 | "scripts": {
5 | "start": "npx shadow-cljs watch :app",
6 | "release": "npx shadow-cljs release :app",
7 | "server": "npx shadow-cljs server"
8 | },
9 | "devDependencies": {
10 | "shadow-cljs": "3.2.1"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/example/todomvc/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "todomvc",
3 | "private": true,
4 | "scripts": {
5 | "start": "npx shadow-cljs watch :app",
6 | "release": "npx shadow-cljs release :app",
7 | "server": "npx shadow-cljs server"
8 | },
9 | "devDependencies": {
10 | "shadow-cljs": "3.2.1"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/test/vrac/test/util.cljc:
--------------------------------------------------------------------------------
1 | (ns vrac.test.util)
2 |
3 | (defn make-gensym
4 | "Provides a function which returns symbols consistently & deterministically."
5 | []
6 | (let [n (atom 0)]
7 | (fn gensym
8 | ([] (gensym "G__"))
9 | ([prefix-string]
10 | (symbol (str prefix-string (swap! n inc)))))))
11 |
--------------------------------------------------------------------------------
/example/si-frame-simple/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "si-frame-simple",
3 | "private": true,
4 | "scripts": {
5 | "start": "npx shadow-cljs watch :app",
6 | "release": "npx shadow-cljs release :app",
7 | "server": "npx shadow-cljs server"
8 | },
9 | "devDependencies": {
10 | "shadow-cljs": "3.2.1"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/example/todomvc/shadow-cljs.edn:
--------------------------------------------------------------------------------
1 | {:deps true
2 |
3 | :dev-http {3000 "public"}
4 |
5 | :builds {:app {:target :browser
6 | :output-dir "public/js"
7 | :asset-path "/js"
8 | :modules {:main {:init-fn todomvc.core/start}}
9 | :build-hooks [(shadow.cljs.build-report/hook)]}}}
10 |
--------------------------------------------------------------------------------
/example/si-frame-simple/shadow-cljs.edn:
--------------------------------------------------------------------------------
1 | {:deps true
2 |
3 | :dev-http {3000 "public"}
4 |
5 | :builds {:app {:target :browser
6 | :output-dir "public/js"
7 | :asset-path "/js"
8 | :modules {:main {:init-fn simple.core/run}}
9 | :build-hooks [(shadow.cljs.build-report/hook)]}}}
10 |
--------------------------------------------------------------------------------
/example/test-app/shadow-cljs.edn:
--------------------------------------------------------------------------------
1 | {:deps true
2 |
3 | :dev-http {3000 "public"}
4 |
5 | :builds {:app {:target :browser
6 | :output-dir "public/js"
7 | :asset-path "/js"
8 | :modules {:main {:init-fn example.core/start-app}}
9 | :build-hooks [(shadow.cljs.build-report/hook)]}}}
10 |
--------------------------------------------------------------------------------
/example/react-interop/shadow-cljs.edn:
--------------------------------------------------------------------------------
1 | {:deps true
2 |
3 | :dev-http {3000 "public"}
4 |
5 | :builds {:app {:target :browser
6 | :output-dir "public/js"
7 | :asset-path "/js"
8 | :modules {:main {:init-fn react-interop.core/start-app}}
9 | :build-hooks [(shadow.cljs.build-report/hook)]}}}
10 |
--------------------------------------------------------------------------------
/example/react-interop/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Vrac + React
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/example/test-app/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Vrac test app
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/example/si-frame-simple/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Vrac + Si-frame
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/example/react-interop/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-interop",
3 | "private": true,
4 | "scripts": {
5 | "start": "npx shadow-cljs watch :app",
6 | "release": "npx shadow-cljs release :app",
7 | "server": "npx shadow-cljs server"
8 | },
9 | "devDependencies": {
10 | "shadow-cljs": "3.2.1"
11 | },
12 | "dependencies": {
13 | "react": "^19.2.0",
14 | "react-dom": "^19.2.0"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/example/todomvc/src/todomvc/cofx.cljs:
--------------------------------------------------------------------------------
1 | (ns todomvc.cofx
2 | (:require [cljs.reader :as reader]
3 | [re-frame.core :as rf]
4 | [todomvc.constant :as const]))
5 |
6 | (rf/reg-cofx
7 | :todo-items-from-local-store
8 | (fn [cofx _]
9 | (assoc cofx
10 | :todo-items-from-local-store
11 | (some-> (js/localStorage.getItem const/todo-item-local-storage-key)
12 | reader/read-string))))
13 |
--------------------------------------------------------------------------------
/example/test-app/README.md:
--------------------------------------------------------------------------------
1 | # Vrac test app
2 |
3 | ## How to run this project
4 |
5 | ```shell
6 | npm install
7 | npm start
8 | ```
9 |
10 | This will run Shadow-CLJS. Once the project finished to compile,
11 | follow the [displayed link](http://localhost:3000) to see the app.
12 |
13 | ## Production build
14 |
15 | Compiling for production:
16 |
17 | ```shell
18 | npm run release
19 | ```
20 |
21 | The report describing the size of the different parts is `public/js/report.html`.
22 |
--------------------------------------------------------------------------------
/example/todomvc/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | TodoMVC Hacking
6 |
7 |
8 |
9 |
10 |
11 | Loading...
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/example/react-interop/src/react_interop/core.cljs:
--------------------------------------------------------------------------------
1 | (ns react-interop.core
2 | (:require [vrac.web :as vw :refer [$]]
3 | [react-interop.interop :refer [interop-demo]]))
4 |
5 | ;; Shadow-CLJS hooks: start & reload the app
6 |
7 | (defn ^:dev/after-load setup! []
8 | (vw/render (js/document.getElementById "app")
9 | ($ interop-demo)))
10 |
11 | (defn ^:dev/before-load shutdown! []
12 | (vw/dispose-render-effects))
13 |
14 | (defn start-app []
15 | (setup!))
16 |
--------------------------------------------------------------------------------
/example/test-app/deps.edn:
--------------------------------------------------------------------------------
1 | {:paths ["src" "resource"]
2 |
3 | :deps {org.clojure/clojure {:mvn/version "1.12.3"}
4 | org.clojure/clojurescript {:mvn/version "1.12.42"}
5 | thheller/shadow-cljs {:mvn/version "3.2.1"}
6 | taipei.404.vrac/vrac {:local/root "../.."}}
7 |
8 | :aliases {;; clojure -M:outdated --upgrade
9 | :outdated {:extra-deps {com.github.liquidz/antq {:mvn/version "2.11.1276"}}
10 | :main-opts ["-m" "antq.core"]}}}
11 |
--------------------------------------------------------------------------------
/example/todomvc/README.md:
--------------------------------------------------------------------------------
1 | # TodoMVC
2 |
3 | A famous sample app implemented using Vrac.
4 |
5 | ## How to run this project
6 |
7 | ```shell
8 | npm install
9 | npm start
10 | ```
11 |
12 | This will run Shadow-CLJS. Once the project finished to compile,
13 | follow the [displayed link](http://localhost:3000) to see the app.
14 |
15 | ## Production build
16 |
17 | Compiling for production:
18 |
19 | ```shell
20 | npm run release
21 | ```
22 |
23 | The report describing the size of the different parts is `public/js/report.html`.
24 |
--------------------------------------------------------------------------------
/example/todomvc/src/todomvc/router.cljs:
--------------------------------------------------------------------------------
1 | (ns todomvc.router
2 | (:require [re-frame.core :as rf]
3 | [reitit.frontend :as rtf]
4 | [reitit.frontend.easy :as rtfe]))
5 |
6 | (def routes
7 | ["/"
8 | ["" {:name :page/all-todo-items}]
9 | ["active" {:name :page/active-todo-items}]
10 | ["completed" {:name :page/completed-todo-items}]])
11 |
12 | (defn start-router! []
13 | (rtfe/start!
14 | (rtf/router routes)
15 | (fn [m]
16 | (rf/dispatch [:router/set-route-match m]))
17 | {:use-fragment true}))
18 |
--------------------------------------------------------------------------------
/example/si-frame-simple/deps.edn:
--------------------------------------------------------------------------------
1 | {:paths ["src"]
2 |
3 | :deps {org.clojure/clojure {:mvn/version "1.12.3"}
4 | org.clojure/clojurescript {:mvn/version "1.12.42"}
5 | thheller/shadow-cljs {:mvn/version "3.2.1"}
6 | taipei.404.vrac/vrac {:local/root "../.."}
7 | fi.metosin/si-frame {:mvn/version "1.4.3.0-no-siagent"}}
8 |
9 | :aliases {;; clojure -M:outdated --upgrade
10 | :outdated {:extra-deps {com.github.liquidz/antq {:mvn/version "2.11.1276"}}
11 | :main-opts ["-m" "antq.core"]}}}
12 |
--------------------------------------------------------------------------------
/example/si-frame-simple/README.md:
--------------------------------------------------------------------------------
1 | # Vrac + Si-frame, "simple" demo ported from Re-frame
2 |
3 | This project shows how to use Si-frame together with Vrac.
4 |
5 | ## How to run this project
6 |
7 | ```shell
8 | npm install
9 | npm start
10 | ```
11 |
12 | This will run Shadow-CLJS. Once the project finished to compile,
13 | follow the [displayed link](http://localhost:3000) to see the app.
14 |
15 | ## Production build
16 |
17 | Compiling for production:
18 |
19 | ```shell
20 | npm run release
21 | ```
22 |
23 | The report describing the size of the different parts is `public/js/report.html`.
24 |
--------------------------------------------------------------------------------
/example/todomvc/src/todomvc/interceptor.cljs:
--------------------------------------------------------------------------------
1 | (ns todomvc.interceptor
2 | (:require [re-frame.core :as rf]
3 | [todomvc.constant :as const]))
4 |
5 | (def save-todo-items-in-local-storage
6 | (rf/->interceptor
7 | :id :save-todo-items-in-local-storage
8 | :after (fn [context]
9 | (let [db (or (get-in context [:effects :db])
10 | (get-in context [:coeffects :db]))]
11 | (update-in context [:effects :fx] conj
12 | [:save-in-local-store {:key const/todo-item-local-storage-key
13 | :value (str (:todo-items db))}])))))
14 |
--------------------------------------------------------------------------------
/example/todomvc/deps.edn:
--------------------------------------------------------------------------------
1 | {:paths ["src"]
2 |
3 | :deps {org.clojure/clojure {:mvn/version "1.12.3"}
4 | org.clojure/clojurescript {:mvn/version "1.12.42"}
5 | thheller/shadow-cljs {:mvn/version "3.2.1"}
6 | metosin/reitit-frontend {:mvn/version "0.9.1"}
7 | taipei.404.vrac/vrac {:local/root "../.."}
8 | fi.metosin/si-frame {:mvn/version "1.4.3.0-no-siagent"}}
9 |
10 |
11 | :aliases {; clojure -M:outdated --upgrade
12 | :outdated {:extra-deps {com.github.liquidz/antq {:mvn/version "2.11.1276"}}
13 | :main-opts ["-m" "antq.core"]}}}
14 |
--------------------------------------------------------------------------------
/.github/workflows/cljdoc.yml:
--------------------------------------------------------------------------------
1 | name: CljDoc
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 |
9 | jobs:
10 |
11 | check-cljdoc:
12 |
13 | name: Check source code
14 |
15 | timeout-minutes: 60
16 |
17 | runs-on: ubuntu-latest
18 |
19 | steps:
20 | - uses: actions/checkout@v5
21 |
22 | - name: Setup Clojure
23 | uses: DeLaGuardo/setup-clojure@master
24 | with:
25 | cli: latest
26 |
27 | - name: Build the jar and update pom.xml's version
28 | run: clojure -X:jar && mkdir target && mv *.jar target/
29 |
30 | - name: CljDoc Check
31 | uses: cljdoc/cljdoc-check-action@v0.0.3
32 |
--------------------------------------------------------------------------------
/src/vrac/dsl.cljc:
--------------------------------------------------------------------------------
1 | (ns ^:no-doc vrac.dsl)
2 |
3 | ;; Keywords used in Vrac's DSL which are already defined in clojure.core/cljs.core:
4 | ;; defn, let, when, for
5 |
6 | (def reserved-symbols '[$
7 | do if quote ;; <- keywords of the Clojure languages, not defined in clojure.core
8 | global context with-context
9 | once signal state memo
10 | effect effect-on on-clean-up])
11 |
12 | ;; Those declarations help making the IDEs happy about the user's DSL expressions.
13 | (declare $
14 | global context with-context
15 | once signal state memo
16 | effect effect-on on-clean-up)
17 |
--------------------------------------------------------------------------------
/doc/ai-context-construction-prompt.txt:
--------------------------------------------------------------------------------
1 | Ultrathink: Read @src/vrac/web.cljc, @doc/vcup-properties.md, the files in directory @example/test-app/src/example/ and the @example/si-frame-simple/src/simple/core.cljs.
2 | From the patterns of how Vrac is used in those correct programs, I want you to write a summary of those patterns in
3 | @doc/ai-context.md which will be used by Claude Code for generating Clojurescript programs using Vrac, Si-Frame and Signaali.
4 | The information should not be application-specific. Only include the information which is really needed for generating a correct program,
5 | and avoid repeating yourself.
6 | Latest version of Vrac to use in file deps.edn is `taipei.404.vrac/vrac {:mvn/version "0.1.2"}`
7 |
--------------------------------------------------------------------------------
/example/react-interop/deps.edn:
--------------------------------------------------------------------------------
1 | {:paths ["src"]
2 |
3 | :deps {org.clojure/clojure {:mvn/version "1.12.3"}
4 | org.clojure/clojurescript {:mvn/version "1.12.42"}
5 | thheller/shadow-cljs {:mvn/version "3.2.1"}
6 | taipei.404.vrac/vrac {:local/root "../.."}
7 | fi.metosin/si-frame {:mvn/version "1.4.3.0"}
8 | fi.metosin/siagent {:mvn/version "0.1.0"}
9 | com.pitch/uix.core {:mvn/version "1.4.5"}
10 | com.pitch/uix.dom {:mvn/version "1.4.5"}}
11 |
12 | :aliases {;; clojure -M:outdated --upgrade
13 | :outdated {:extra-deps {com.github.liquidz/antq {:mvn/version "2.11.1276"}}
14 | :main-opts ["-m" "antq.core"]}}}
15 |
--------------------------------------------------------------------------------
/example/react-interop/README.md:
--------------------------------------------------------------------------------
1 | # Vrac + React 19, interop demo
2 |
3 | This project shows how to use Vrac and React 19 together.
4 | 1. How to embed React roots and components inside a Vrac app.
5 | 2. How to use the reactive data from the Vrac app inside the embedded React components.
6 | The source code shows how to do it:
7 | 1. using Siagent: just deref the reactive nodes, "et voilà".
8 | 2. using UIx: use the `use-reactive` React hook.
9 |
10 | ## How to run this project
11 |
12 | ```shell
13 | npm install
14 | npm start
15 | ```
16 |
17 | This will run Shadow-CLJS. Once the project finished to compile,
18 | follow the [displayed link](http://localhost:3000) to see the app.
19 |
20 | ## Production build
21 |
22 | Compiling for production:
23 |
24 | ```shell
25 | npm run release
26 | ```
27 |
28 | The report describing the size of the different parts is `public/js/report.html`.
29 |
--------------------------------------------------------------------------------
/example/todomvc/src/todomvc/core.cljs:
--------------------------------------------------------------------------------
1 | (ns todomvc.core
2 | (:require [re-frame.core :as rf]
3 | [todomvc.cofx]
4 | [todomvc.fx]
5 | [todomvc.interceptor]
6 | [todomvc.event]
7 | [todomvc.sub]
8 | [todomvc.router :as router]
9 | [todomvc.view :as view]
10 | [vrac.web :as vw :refer [$]]))
11 |
12 | (defn mount-ui []
13 | (vw/render (js/document.getElementById "app")
14 | ($ view/main-page)))
15 |
16 | (defn ^:dev/after-load clear-cache-and-render! []
17 | (rf/clear-subscription-cache!)
18 | (router/start-router!)
19 | (mount-ui))
20 |
21 | (defn ^:dev/before-load shutdown! []
22 | (vw/dispose-render-effects))
23 |
24 | (defn start []
25 | (rf/dispatch-sync [:initialize-db])
26 | (rf/dispatch-sync [:local-storage/load-todo-items])
27 | (router/start-router!)
28 | (mount-ui))
29 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6 |
7 | ## [Unreleased]
8 |
9 | ### Added
10 |
11 | - A documentation for LLMs.
12 |
13 | ## [0.1.2] - 2025-10-04
14 |
15 | ### Fixed
16 |
17 | - Cljdoc documentation is fixed.
18 | - Fixed a bug which prevented reactive fragments (e.g. if, for) to be used directly inside each other.
19 |
20 | ### Changed
21 |
22 | - Bumped the dependencies on the example projects, notably Shadow-CLJS.
23 |
24 | ## [0.1.1] - 2025-04-24
25 |
26 | ### Changed
27 |
28 | - Uses a new format for naming the attributes, properties and event handlers (#5).
29 | - `vrac.web/attributes-effect` renamed to `vrac.web/props-effect`.
30 |
31 | ### Added
32 |
33 | - Documentation about the attributes and properties in Vcup.
34 |
35 | ## [0.1.0] - 2025-03-17
36 |
37 | ### Added
38 |
39 | - the `vrac.web` namespace
40 | - some examples in the `/examples` directory
41 |
--------------------------------------------------------------------------------
/example/test-app/src/example/math_ml.cljs:
--------------------------------------------------------------------------------
1 | (ns example.math-ml
2 | (:require [vrac.web :as vw :refer [$]]))
3 |
4 | (defn- sample1-article []
5 | ($ :article
6 | ($ :h2 "MathML sample 1")
7 | ($ :math {:display "block"}
8 | ($ :mrow
9 | ($ :mrow
10 | ($ :mo "|")
11 | ($ :mi "x")
12 | ($ :mo "|"))
13 | ($ :mo "=")
14 | ($ :mi "x"))
15 | ($ :mtext "\u00a0iff\u00a0")
16 | ($ :mrow
17 | ($ :mi "x")
18 | ($ :mo "≥")
19 | ($ :mn "0")))))
20 |
21 | (defn- sample2-article []
22 | ($ :article
23 | ($ :h2 "MathML sample 2")
24 | ($ :math {:display "block"}
25 | ($ :mfrac
26 | ($ :mrow
27 | ($ :mi "a")
28 | ($ :mo "+")
29 | ($ :mn "2"))
30 | ($ :mrow
31 | ($ :mn "3")
32 | ($ :mo "−")
33 | ($ :mi "b"))))))
34 |
35 | (defn math-ml-root []
36 | ($ :div
37 | ($ sample1-article)
38 | ($ sample2-article)))
39 |
--------------------------------------------------------------------------------
/playwright/tests/vcup.spec.ts:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | import { test, expect } from "@playwright/test";
3 |
4 | test.beforeEach(async ({ page }) => {
5 | await page.goto("/");
6 | });
7 |
8 | test("vcup-root", async ({ page }) => {
9 | const vcupRoot = page.getByTestId("vcup-root");
10 | await expect(vcupRoot).toMatchAriaSnapshot(`
11 | - article:
12 | - heading "Text nodes" [level=2]
13 | - heading "Text" [level=3]
14 | - text: Hello, world!
15 | - heading "Unicode characters" [level=3]
16 | - text: There is two non-breakable spaces between A and B.
17 | - heading "Numbers" [level=3]
18 | - text: 1 2 3
19 | - heading "Booleans" [level=3]
20 | - text: false true
21 | - heading "Keywords" [level=3]
22 | - text: :foo :bar :foo/bar
23 | - heading "Vectors" [level=3]
24 | - text: "[:div {:style {:color \\\"lime\\\"}} 1 2]"
25 | - 'heading "\`nil\` values" [level=3]'
26 | - article:
27 | - heading "Image" [level=2]
28 | - img
29 | - article:
30 | - heading "Vcup elements with funny names" [level=2]
31 | - text: :span#id1.class1.class2 :#id2.class3 :.class4
32 | - article:
33 | - heading "Fragments" [level=2]
34 | - text: 1a1b2a2b
35 | - article:
36 | - heading "Component with arguments" [level=2]
37 | - code: (is 123 (+ a b c))
38 | `);
39 | });
40 |
--------------------------------------------------------------------------------
/example/test-app/src/example/core.cljs:
--------------------------------------------------------------------------------
1 | (ns example.core
2 | (:require [example.context :refer [context-root]]
3 | [example.math-ml :refer [math-ml-root]]
4 | [example.reactive-data :refer [reactive-data-root]]
5 | [example.reactive-fragment :refer [reactive-fragment-root]]
6 | [example.svg :refer [svg-root]]
7 | [example.ref :refer [ref-root]]
8 | [example.vcup :refer [vcup-root]]
9 | [example.ui-component :refer [ui-component-root]]
10 | [signaali.reactive :as sr]
11 | [vrac.web :as vw :refer [$]]))
12 |
13 | (defn- debug-prn [reactive-node event-type]
14 | (when-some [name (-> reactive-node meta :name)]
15 | (prn name event-type)))
16 |
17 | ;; Print the debug info in the Browser's console.
18 | ;;(set! sr/*notify-lifecycle-event* debug-prn)
19 |
20 | (defn root-component []
21 | ($ :main
22 | ($ vcup-root)
23 | ($ reactive-data-root)
24 | ($ reactive-fragment-root)
25 | ($ ref-root)
26 | ($ context-root)
27 | ($ svg-root)
28 | ($ math-ml-root)
29 | ($ ui-component-root)
30 | ,))
31 |
32 | ;; Shadow-CLJS hooks: start & reload the app
33 |
34 | (defn ^:dev/after-load setup! []
35 | (vw/render (js/document.getElementById "app")
36 | ($ root-component)))
37 |
38 | (defn ^:dev/before-load shutdown! []
39 | (vw/dispose-render-effects))
40 |
41 | (defn start-app []
42 | (setup!))
43 |
--------------------------------------------------------------------------------
/example/test-app/src/example/reactive_data.cljs:
--------------------------------------------------------------------------------
1 | (ns example.reactive-data
2 | (:require [clojure.string :as str]
3 | [signaali.reactive :as sr]
4 | [vrac.web :as vw :refer [$]]))
5 |
6 | ;; Updating the state makes the DOM update
7 | (defn- counter-component [counter-state]
8 | ($ :div
9 | "Counter value: " counter-state
10 | ($ :div
11 | ($ :button {:on/click #(swap! counter-state inc)} "Increment")
12 | ($ :button {:on/click #(reset! counter-state 0)} "Reset"))))
13 |
14 | (defn- counters-article []
15 | ($ :article
16 | ($ :h2 "Reactive counters")
17 | (for [i (range 3)]
18 | (let [counter-state (sr/create-state (* i 100))]
19 | ($ counter-component counter-state)))))
20 |
21 | (defn- controlled-input-article []
22 | ($ :article
23 | ($ :h2 "Controlled input")
24 | ($ :div "This input prevents its content from containing \"foobar\".")
25 | (let [text-signal (sr/create-signal "foo")]
26 | ($ :input
27 | (vw/props-effect (fn [] {:value @text-signal}))
28 | {:on/input (fn [^js event]
29 | (let [text (-> event .-target .-value)]
30 | (swap! text-signal (fn [previous-text]
31 | (if (str/includes? text "foobar")
32 | previous-text
33 | text)))))}))))
34 |
35 | (defn reactive-data-root []
36 | ($ :div
37 | ($ counters-article)
38 | ($ controlled-input-article)))
39 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Vrac
2 |
3 | [](https://clojars.org/taipei.404.vrac/vrac)
4 | [](https://clojurians.slack.com/app_redirect?channel=vrac)
5 | [](https://cljdoc.org/d/taipei.404.vrac/vrac)
6 |
7 | > The only way to stop using JS is to stop using JS.
8 |
9 | Vrac is a frontend rendering library in Clojure, for Clojurists.
10 |
11 | Features implemented so far:
12 | - 100% Written in Clojure(script)
13 | - Uses signals to do efficient fine-grained updates on the DOM
14 | - Has a good interop with React and Clojurescript React wrappers
15 | - 100% compatible with Re-frame via the fork [Si-frame](https://github.com/metosin/si-frame)
16 | - Has [concise and simple examples](example) showing how to use it
17 | - Carefully crafted [AI context for LLMs and AI agents](doc/ai-context.md).
18 |
19 | Features on the roadmap:
20 | - Declarative developer experience via a DSL
21 | - Pluggable data management
22 | - Transparent reactivity
23 | - In browser dev tools
24 | - Hot reloading
25 | - Client-server data sync
26 |
27 | ## Project status
28 |
29 | This project is currently in its early development.
30 |
31 | It doesn't have known bugs, is implemented enough to be useful to small projects
32 | but might not have enough built-in features to be used in large projects by large teams.
33 |
34 | ## License
35 |
36 | This project is distributed under the [Eclipse Public License v2.0](LICENSE).
37 |
38 | Copyright (c) Vincent Cantin and contributors.
39 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 4.0.0
4 | jar
5 | taipei.404.vrac
6 | vrac
7 | 0.1.2
8 | vrac
9 | A frontend rendering library in Clojure, for Clojurists
10 | 2025
11 |
12 |
13 | EPL-2.0
14 | https://opensource.org/license/EPL-2.0
15 |
16 |
17 |
18 | https://github.com/green-coder/vrac
19 | scm:git:git://github.com/green-coder/vrac.git
20 | scm:git:ssh://git@github.com/green-coder/vrac.git
21 | v0.1.2
22 |
23 |
24 |
25 | org.clojure
26 | clojure
27 | 1.12.3
28 |
29 |
30 | fi.metosin
31 | signaali
32 | 0.1.0
33 |
34 |
35 |
36 | src
37 |
38 |
39 |
40 | clojars
41 | https://repo.clojars.org/
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/.github/workflows/validate.yml:
--------------------------------------------------------------------------------
1 | name: Validate
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 |
9 | jobs:
10 |
11 | test:
12 |
13 | name: Unit and e2e tests
14 |
15 | timeout-minutes: 60
16 |
17 | runs-on: ubuntu-latest
18 |
19 | steps:
20 | - uses: actions/checkout@v5
21 |
22 | - uses: actions/setup-node@v5
23 | with:
24 | node-version: lts/hydrogen
25 |
26 | - name: Setup java
27 | uses: actions/setup-java@v3
28 | with:
29 | distribution: 'temurin'
30 | java-version: '21'
31 |
32 | - name: Setup Clojure
33 | uses: DeLaGuardo/setup-clojure@master
34 | with:
35 | cli: latest
36 |
37 | - name: Cache deps dependencies
38 | uses: actions/cache@v4
39 | with:
40 | path: ~/.m2/repository
41 | key: ${{ runner.os }}-clojure-${{ hashFiles('**/deps.edn') }}
42 | restore-keys: |
43 | ${{ runner.os }}-clojure
44 |
45 | - name: Install dependencies for unit tests
46 | run: npm ci
47 |
48 | - name: Unit tests
49 | run: ./bin/kaocha
50 |
51 | - name: Install dependencies
52 | working-directory: ./playwright
53 | run: npm ci
54 |
55 | - name: Install Playwright Browsers
56 | working-directory: ./playwright
57 | run: npx playwright install --with-deps
58 |
59 | - name: Run Playwright tests
60 | working-directory: ./playwright
61 | run: npm test
62 |
63 | - uses: actions/upload-artifact@v4
64 | if: ${{ !cancelled() }}
65 | with:
66 | name: playwright-report
67 | path: playwright/playwright-report/
68 | retention-days: 30
69 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | release:
5 | types:
6 | - published # reacts to releases and pre-releases, but not their drafts
7 |
8 | jobs:
9 |
10 | test-and-release:
11 |
12 | name: Validate, Jar and Deploy
13 |
14 | timeout-minutes: 60
15 |
16 | runs-on: ubuntu-latest
17 |
18 | steps:
19 | - uses: actions/checkout@v5
20 |
21 | - uses: actions/setup-node@v5
22 | with:
23 | node-version: lts/hydrogen
24 |
25 | - name: Setup java
26 | uses: actions/setup-java@v3
27 | with:
28 | distribution: 'temurin'
29 | java-version: '21'
30 |
31 | - name: Setup Clojure
32 | uses: DeLaGuardo/setup-clojure@master
33 | with:
34 | cli: latest
35 |
36 | - name: Install dependencies for unit tests
37 | run: npm ci
38 |
39 | - name: Unit tests
40 | run: ./bin/kaocha
41 |
42 | - name: Install dependencies
43 | working-directory: ./playwright
44 | run: npm ci
45 |
46 | - name: Install Playwright Browsers
47 | working-directory: ./playwright
48 | run: npx playwright install --with-deps
49 |
50 | - name: Run Playwright tests
51 | working-directory: ./playwright
52 | run: npm test
53 |
54 | - uses: actions/upload-artifact@v4
55 | if: ${{ !cancelled() }}
56 | with:
57 | name: playwright-report
58 | path: playwright/playwright-report/
59 | retention-days: 30
60 |
61 | - name: Build the jar and update pom.xml's version
62 | run: clojure -X:jar
63 |
64 | - name: Deploy the jar and pom files to Clojars
65 | run: clojure -X:deploy
66 | env:
67 | CLOJARS_USERNAME: green-coder
68 | CLOJARS_PASSWORD: "${{ secrets.CLOJARS_DEPLOY_TOKEN }}"
69 |
--------------------------------------------------------------------------------
/example/test-app/src/example/ref.cljs:
--------------------------------------------------------------------------------
1 | (ns example.ref
2 | (:require [signaali.reactive :as sr]
3 | [vrac.web :as vw :refer [$]]))
4 |
5 | (defn- ref-signal-article [element-ref]
6 | ($ :article
7 | ($ :h2 "Capture an element in a reactive signal via the special prop `ref`")
8 | (let [is-existing (sr/create-signal false)]
9 | ($ :div
10 | ($ :button {:on/click (fn [] (swap! is-existing not))}
11 | (sr/create-derived (fn [] (if @is-existing "Destroy element" "Create element"))))
12 | " "
13 | (vw/when-fragment is-existing
14 | ($ :span {:ref element-ref} "Referred element"))))))
15 |
16 | (defn- using-ref-somewhere-else-article1 [element-ref]
17 | ($ :article
18 | ($ :h2 "Use the ref's innerText via a derived data.")
19 | ($ :div (sr/create-derived
20 | (fn []
21 | (if-some [^js element @element-ref]
22 | (.-innerText element)
23 | "["))))))
24 |
25 | (defn- using-ref-somewhere-else-article2 [element-ref]
26 | ($ :article
27 | ($ :h2 "Use the ref to change a color reactively.")
28 | "The square is:"
29 | ($ :ul
30 | ($ :li ($ :strong "red") " when the ref is nil, and")
31 | ($ :li ($ :strong "green") " when the ref is set to an element."))
32 | ($ :div
33 | {:style {:width "50px"
34 | :height "50px"}}
35 | (vw/props-effect
36 | (fn []
37 | {:style {:background-color (if (nil? @element-ref)
38 | "red"
39 | "green")}})))))
40 |
41 | (defn ref-root []
42 | (let [element-ref (sr/create-signal nil)]
43 | ($ :div
44 | ($ ref-signal-article element-ref)
45 | ($ using-ref-somewhere-else-article1 element-ref)
46 | ($ using-ref-somewhere-else-article2 element-ref))))
47 |
--------------------------------------------------------------------------------
/deps.edn:
--------------------------------------------------------------------------------
1 | {:paths ["src"]
2 |
3 | :deps {fi.metosin/signaali {:mvn/version "0.1.0"}
4 | ;; This is not used by Vrac yet
5 | taipei.404/mate {:mvn/version "0.1.0"}}
6 |
7 | :aliases {:dev {:extra-deps {org.clojure/clojure {:mvn/version "1.12.3"}
8 | org.clojure/clojurescript {:mvn/version "1.12.42"}
9 | djblue/portal {:mvn/version "0.61.0"}
10 | lambdaisland/deep-diff2 {:mvn/version "2.12.219"}}}
11 |
12 | :test {:extra-paths ["test" "test-resource"]
13 | :extra-deps {lambdaisland/kaocha {:mvn/version "1.91.1392"}
14 | lambdaisland/kaocha-cljs {:mvn/version "1.8.163"}
15 | lambdaisland/kaocha-junit-xml {:mvn/version "1.17.101"}
16 | org.clojure/test.check {:mvn/version "1.1.1"}
17 | org.clojure/data.json {:mvn/version "2.5.1"}}}
18 |
19 | ;; clojure -M:outdated --upgrade
20 | :outdated {:extra-deps {com.github.liquidz/antq {:mvn/version "2.11.1276"}}
21 | :main-opts ["-m" "antq.core"]}
22 |
23 | :jar {:replace-deps {com.github.seancorfield/depstar {:mvn/version "2.1.303"}}
24 | :exec-fn hf.depstar/jar
25 | :exec-args {:sync-pom true
26 | :group-id "taipei.404.vrac"
27 | :artifact-id "vrac"
28 | :version "0.1.2"
29 | :jar "vrac.jar"}}
30 |
31 | :deploy {:extra-deps {slipset/deps-deploy {:mvn/version "0.2.2"}}
32 | :exec-fn deps-deploy.deps-deploy/deploy
33 | :exec-args {:installer :remote
34 | :artifact "vrac.jar"}}}}
35 |
--------------------------------------------------------------------------------
/example/todomvc/src/todomvc/sub.cljs:
--------------------------------------------------------------------------------
1 | (ns todomvc.sub
2 | (:require [re-frame.core :as rf]))
3 |
4 | (rf/reg-sub
5 | :router/route-match
6 | (fn [db _]
7 | (:router/route-match db)))
8 |
9 | (rf/reg-sub
10 | :comp.input/title
11 | (fn [db _]
12 | (:comp.input/title db)))
13 |
14 | (rf/reg-sub
15 | :comp.input/any-todo-item
16 | (fn [db _]
17 | (pos? (count (:todo-items db)))))
18 |
19 | (rf/reg-sub
20 | :all-todo-items-completed
21 | (fn [db _]
22 | (->> (vals (:todo-items db))
23 | (every? :completed))))
24 |
25 | (rf/reg-sub
26 | :todo-item-ids
27 | (fn [db _]
28 | (keys (:todo-items db))))
29 |
30 | (rf/reg-sub
31 | :todo-item
32 | (fn [db [_ todo-item-id]]
33 | (-> db :todo-items (get todo-item-id))))
34 |
35 | (rf/reg-sub
36 | :comp/display-type
37 | :<- [:router/route-match]
38 | (fn [router-match _]
39 | (-> router-match :data :name {:page/all-todo-items :all
40 | :page/active-todo-items :active
41 | :page/completed-todo-items :completed})))
42 |
43 | (rf/reg-sub
44 | :show-todo-item
45 | (fn [[_ todo-item-id]]
46 | [(rf/subscribe [:comp/display-type])
47 | (rf/subscribe [:todo-item todo-item-id])])
48 | (fn [[display-type todo-item] _]
49 | (let [completed (:completed todo-item)]
50 | (case display-type
51 | :completed completed
52 | :active (not completed)
53 | #_:all true))))
54 |
55 | (rf/reg-sub
56 | :active-todo-items
57 | (fn [db _]
58 | (->> (vals (:todo-items db))
59 | (filterv (complement :completed)))))
60 |
61 | (rf/reg-sub
62 | :completed-todo-items
63 | (fn [db _]
64 | (->> (vals (:todo-items db))
65 | (filterv :completed))))
66 |
67 | (rf/reg-sub
68 | :active-todo-items-count
69 | :<- [:active-todo-items]
70 | (fn [active-todo-items _]
71 | (count active-todo-items)))
72 |
73 | (rf/reg-sub
74 | :complemented-todo-items-count
75 | :<- [:completed-todo-items]
76 | (fn [completed-todo-items _]
77 | (count completed-todo-items)))
78 |
79 | (rf/reg-sub
80 | :complemented-todo-items-to-clear
81 | :<- [:complemented-todo-items-count]
82 | (fn [complemented-todo-items-count _]
83 | (pos? complemented-todo-items-count)))
84 |
--------------------------------------------------------------------------------
/doc/vcup-properties.md:
--------------------------------------------------------------------------------
1 | ## Vcup properties
2 |
3 | Also named "props" in the source code, the Vcup properties are always specified using Clojure keywords.
4 |
5 | When the keyword's namespace is `"a"` (e.g. `:a/for`),
6 | the keyword refers to [an HTML attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/for).
7 |
8 | When the keyword's namespace is `"p"` (e.g. `:p/htmlFor`),
9 | the keyword refers to [a DOM element's property](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/htmlFor).
10 |
11 | When the keyword's namespace is `"on"` (e.g. `:on/click`),
12 | the keyword refers to [an event](https://developer.mozilla.org/en-US/docs/Web/API/Element/click_event),
13 | and the value in an event handler function.
14 |
15 | When the keyword has no namespace, its meaning depends on its name:
16 | 1. Either the keyword refers to some Vrac specific properties which provide
17 | additional convenience to the user like `:style`, `:class`, `:ref`, or
18 | 2. if the keyword starts with `"data-"` then it represents
19 | [a custom data attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/data-*)
20 | (e.g. `:data-testid`), or else
21 | 3. if the prop is applied on an SVG on MathML element then it represents an attribute, or else
22 | 4. it represents a DOM element's property (e.g. `:htmlFor`, which is equivalent to `:p/htmlFor`).
23 |
24 | ### Letter case in prop names
25 |
26 | Vrac doesn't modify the letter case, the user is directly speaking the browser's language.
27 |
28 | In practice, it means that:
29 | 1. Attributes names are [the same as specified in this page](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes),
30 | e.g.:
31 | 1. `:a/readonly`
32 | 2. `:a/tabindex`
33 | 3. `:a/accept-charset`
34 | 2. DOM element's properties names are [the same as specified in this page](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement#instance_properties),
35 | e.g.:
36 | 1. `:p/readOnly` or `:readOnly`
37 | 2. `:p/tabIndex` or `:tabIndex`
38 | 3. `:p/autocorrect` or `:autocorrect`
39 | 3. Event names are [the same as specified in this page](https://developer.mozilla.org/en-US/docs/Web/API/Element#events),
40 | e.g.:
41 | 1. `:on/dblclick`
42 | 2. `:on/keydown`
43 | 3. `:on/fullscreenchange`
44 |
--------------------------------------------------------------------------------
/example/test-app/src/example/ui_component.cljs:
--------------------------------------------------------------------------------
1 | (ns example.ui-component
2 | (:require [clojure.string :as str]
3 | [signaali.reactive :as sr]
4 | [vrac.web :as vw :refer [$]]))
5 |
6 | ;; ----------------------------------------------
7 | ;; UI library's API
8 |
9 | (defn- component-from-context [component-key]
10 | (fn [props & children]
11 | (let [context (vw/get-context)
12 | component-resolver (sr/create-memo
13 | (fn []
14 | (get @context component-key)))]
15 | (vw/reactive-fragment
16 | (fn []
17 | ($ @component-resolver props children))))))
18 |
19 | (def button
20 | (component-from-context :ui.component/button))
21 |
22 | ;; ----------------------------------------------
23 | ;; UI library's implementation
24 |
25 | ;; Components' implementation
26 | (defn default-button-component [props children]
27 | ($ :button props children))
28 |
29 | ;; Bindings
30 | (def component-registry
31 | {:ui.component/button default-button-component})
32 |
33 | ;; ----------------------------------------------
34 |
35 | (defn my-custom-button [props children]
36 | ($ :div {:style {:display "inline-block"
37 | :background-color "lightblue"
38 | :padding "1em"
39 | :border-radius "0.5em"}}
40 | ($ :button
41 | props
42 | {:style {:color "white"
43 | :border-radius "0.5em"}}
44 | ($ :strong
45 | (map str/upper-case children)))))
46 |
47 | (defn overridable-ui-component []
48 | ($ :article
49 | ($ :h2 "Overridable UI component")
50 |
51 | (vw/with-context (sr/create-signal component-registry)
52 | ($ :div
53 | ;; Default button
54 | ($ button {:style {:background-color "pink"}} "Button")
55 |
56 | (vw/with-context-update (fn [context]
57 | (-> @context
58 | (assoc :ui.component/button my-custom-button)))
59 |
60 | ;; Will resolve to the custom button component,
61 | ;; different behavior, structure and style.
62 | ($ button {:style {:background-color "pink"}} "Button"))))))
63 |
64 | (defn ui-component-root []
65 | ($ :div
66 | ($ overridable-ui-component)))
67 |
--------------------------------------------------------------------------------
/example/test-app/src/example/vcup.cljs:
--------------------------------------------------------------------------------
1 | (ns example.vcup
2 | (:require [vrac.web :as vw :refer [$]]))
3 |
4 | (defn- text-node-article []
5 | ($ :article
6 | ($ :h2 "Text nodes")
7 |
8 | ($ :h3 "Text")
9 | "Hello, " "world!"
10 |
11 | ($ :h3 "Unicode characters")
12 | "There is two non-breakable spaces between A\u00a0and\u00a0B."
13 |
14 | ($ :h3 "Numbers")
15 | 1 " " 2 " " 3
16 |
17 | ($ :h3 "Booleans")
18 | false " " true
19 |
20 | ;; Because it could be mistaken with the element's props, this shall not work.
21 | ;;($ :h3 "Maps")
22 | ;;{:a 1
23 | ;; :b 2}
24 |
25 | ($ :h3 "Keywords")
26 | :foo " " :bar " " :foo/bar
27 |
28 | ($ :h3 "Vectors")
29 | [:div {:style {:color "lime"}} 1 2]
30 |
31 | ($ :h3 "`nil` values")
32 | nil
33 | nil
34 |
35 | ,))
36 |
37 | (defn- image-article []
38 | ($ :article
39 | ($ :h2 "Image")
40 |
41 | ;; Image from the article https://en.wikipedia.org/wiki/Arctic_fox
42 | ($ :img {:src "https://upload.wikimedia.org/wikipedia/commons/8/83/Iceland-1979445_%28cropped_3%29.jpg"
43 | :style {:background-color "grey"
44 | :width "400px"
45 | :height "400px"
46 | :object-fit "contain"}})))
47 |
48 | (defn- element-article []
49 | ($ :article
50 | ($ :h2 "Vcup elements with funny names")
51 | ($ :span#id1.class1.class2 {:class [:class3 :class4]} :span#id1.class1.class2)
52 | ($ :#id2.class3 :#id2.class3)
53 | ($ :.class4 :.class4)))
54 |
55 | (defn- fragment-article []
56 | ($ :article
57 | ($ :h2 "Fragments")
58 | ($ :<>
59 | ($ :<>
60 | "1a"
61 | "1b")
62 | nil
63 | ($ :<>
64 | "2a"
65 | nil
66 | "2b")
67 | ($ :<>
68 | nil
69 | nil)
70 | ($ :<>))))
71 |
72 | (defn- component-with-arguments-article [a b c]
73 | ($ :article
74 | ($ :h2 "Component with arguments")
75 | ($ :pre
76 | ($ :code
77 | "(is " (+ a b c) " (+ a b c))"))))
78 |
79 | (defn vcup-root []
80 | ($ :div {:data-testid "vcup-root"}
81 | ($ text-node-article)
82 | ($ image-article)
83 | ($ element-article)
84 | ($ fragment-article)
85 | ($ component-with-arguments-article 100 20 3)))
86 |
--------------------------------------------------------------------------------
/playwright/playwright.config.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | import { defineConfig, devices } from "@playwright/test";
3 |
4 | /**
5 | * Read environment variables from file.
6 | * https://github.com/motdotla/dotenv
7 | */
8 | // import dotenv from 'dotenv';
9 | // import path from 'path';
10 | // dotenv.config({ path: path.resolve(__dirname, '.env') });
11 |
12 | /**
13 | * @see https://playwright.dev/docs/test-configuration
14 | */
15 | export default defineConfig({
16 | testDir: "./tests",
17 | /* Run tests in files in parallel */
18 | fullyParallel: true,
19 | /* Fail the build on CI if you accidentally left test.only in the source code. */
20 | forbidOnly: !!process.env.CI,
21 | /* Retry on CI only */
22 | retries: process.env.CI ? 2 : 0,
23 | /* Opt out of parallel tests on CI. */
24 | workers: process.env.CI ? 1 : undefined,
25 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */
26 | reporter: "html",
27 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
28 | use: {
29 | /* Base URL to use in actions like `await page.goto('/')`. */
30 | baseURL: "http://127.0.0.1:3000",
31 |
32 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
33 | trace: "on-first-retry",
34 | },
35 |
36 | /* Configure projects for major browsers */
37 | projects: [
38 | {
39 | name: "chromium",
40 | use: { ...devices["Desktop Chrome"] },
41 | },
42 |
43 | {
44 | name: "firefox",
45 | use: { ...devices["Desktop Firefox"] },
46 | },
47 |
48 | {
49 | name: "webkit",
50 | use: { ...devices["Desktop Safari"] },
51 | },
52 |
53 | /* Test against mobile viewports. */
54 | // {
55 | // name: 'Mobile Chrome',
56 | // use: { ...devices['Pixel 5'] },
57 | // },
58 | // {
59 | // name: 'Mobile Safari',
60 | // use: { ...devices['iPhone 12'] },
61 | // },
62 |
63 | /* Test against branded browsers. */
64 | // {
65 | // name: 'Microsoft Edge',
66 | // use: { ...devices['Desktop Edge'], channel: 'msedge' },
67 | // },
68 | // {
69 | // name: 'Google Chrome',
70 | // use: { ...devices['Desktop Chrome'], channel: 'chrome' },
71 | // },
72 | ],
73 |
74 | /* Run your local dev server before starting the tests */
75 | webServer: {
76 | timeout: 60 * 60 * 1000, // Waiting for shadow-cljs to download its dependencies.
77 | command: "cd ../example/test-app && npm install && npm run release && npm run server",
78 | url: "http://127.0.0.1:3000",
79 | reuseExistingServer: !process.env.CI,
80 | },
81 | });
82 |
--------------------------------------------------------------------------------
/example/todomvc/src/todomvc/event.cljs:
--------------------------------------------------------------------------------
1 | (ns todomvc.event
2 | (:require [re-frame.core :as rf]
3 | [todomvc.constant :as const]
4 | [todomvc.interceptor :as interceptor]))
5 |
6 | (rf/reg-event-db
7 | :initialize-db
8 | (fn [db _]
9 | const/initial-db))
10 |
11 | (rf/reg-event-db
12 | :router/set-route-match
13 | (fn [db [_ route-match]]
14 | (assoc db :router/route-match route-match)))
15 |
16 | (rf/reg-event-fx
17 | :local-storage/load-todo-items
18 | [(rf/inject-cofx :todo-items-from-local-store)]
19 | (fn [{:keys [db todo-items-from-local-store]} _]
20 | {:db (-> db
21 | (assoc :todo-items todo-items-from-local-store
22 | :next-todo-item-id (inc (apply max -1 (keys todo-items-from-local-store)))))}))
23 |
24 | (rf/reg-event-db
25 | :comp.input/on-title-changed
26 | (fn [db [_ title]]
27 | (assoc db :comp.input/title title)))
28 |
29 | (rf/reg-event-db
30 | :comp.input/add-todo-item
31 | [interceptor/save-todo-items-in-local-storage]
32 | (fn [db [_ title]]
33 | (let [todo-item-id (:next-todo-item-id db)]
34 | (-> db
35 | (update :next-todo-item-id inc)
36 | (assoc-in [:todo-items todo-item-id] {:id todo-item-id
37 | :title title
38 | :completed false})
39 | (assoc :comp.input/title "")))))
40 |
41 | (rf/reg-event-db
42 | :toggle-all-todo-items
43 | [interceptor/save-todo-items-in-local-storage]
44 | (fn [db [_ all-completed]]
45 | (-> db
46 | (update :todo-items
47 | update-vals
48 | (fn [todo-item]
49 | (assoc todo-item :completed (not all-completed)))))))
50 |
51 | (rf/reg-event-db
52 | :toggle-todo-item
53 | [interceptor/save-todo-items-in-local-storage]
54 | (fn [db [_ todo-item-id]]
55 | (-> db
56 | (update-in [:todo-items todo-item-id :completed] not))))
57 |
58 | (rf/reg-event-db
59 | :delete-todo-item
60 | [interceptor/save-todo-items-in-local-storage]
61 | (fn [db [_ todo-item-id]]
62 | (-> db
63 | (update :todo-items dissoc todo-item-id))))
64 |
65 | (rf/reg-event-db
66 | :set-todo-item-title
67 | [interceptor/save-todo-items-in-local-storage]
68 | (fn [db [_ todo-item-id title]]
69 | (-> db
70 | (assoc-in [:todo-items todo-item-id :title] title))))
71 |
72 | (rf/reg-event-db
73 | :clean-completed-todo-items
74 | [interceptor/save-todo-items-in-local-storage]
75 | (fn [db _]
76 | (-> db
77 | (update :todo-items
78 | (fn [todo-items]
79 | (into {}
80 | (remove (comp :completed val))
81 | todo-items))))))
82 |
--------------------------------------------------------------------------------
/example/test-app/resource/svg/fox-origami.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
23 |
--------------------------------------------------------------------------------
/playwright/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "playwright",
3 | "lockfileVersion": 3,
4 | "requires": true,
5 | "packages": {
6 | "": {
7 | "name": "playwright",
8 | "devDependencies": {
9 | "@playwright/test": "1.55.0",
10 | "@types/node": "24.5.0"
11 | }
12 | },
13 | "node_modules/@playwright/test": {
14 | "version": "1.55.0",
15 | "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.0.tgz",
16 | "integrity": "sha512-04IXzPwHrW69XusN/SIdDdKZBzMfOT9UNT/YiJit/xpy2VuAoB8NHc8Aplb96zsWDddLnbkPL3TsmrS04ZU2xQ==",
17 | "dev": true,
18 | "license": "Apache-2.0",
19 | "dependencies": {
20 | "playwright": "1.55.0"
21 | },
22 | "bin": {
23 | "playwright": "cli.js"
24 | },
25 | "engines": {
26 | "node": ">=18"
27 | }
28 | },
29 | "node_modules/@types/node": {
30 | "version": "24.5.0",
31 | "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.0.tgz",
32 | "integrity": "sha512-y1dMvuvJspJiPSDZUQ+WMBvF7dpnEqN4x9DDC9ie5Fs/HUZJA3wFp7EhHoVaKX/iI0cRoECV8X2jL8zi0xrHCg==",
33 | "dev": true,
34 | "license": "MIT",
35 | "dependencies": {
36 | "undici-types": "~7.12.0"
37 | }
38 | },
39 | "node_modules/fsevents": {
40 | "version": "2.3.2",
41 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
42 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
43 | "dev": true,
44 | "hasInstallScript": true,
45 | "license": "MIT",
46 | "optional": true,
47 | "os": [
48 | "darwin"
49 | ],
50 | "engines": {
51 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
52 | }
53 | },
54 | "node_modules/playwright": {
55 | "version": "1.55.0",
56 | "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.0.tgz",
57 | "integrity": "sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA==",
58 | "dev": true,
59 | "license": "Apache-2.0",
60 | "dependencies": {
61 | "playwright-core": "1.55.0"
62 | },
63 | "bin": {
64 | "playwright": "cli.js"
65 | },
66 | "engines": {
67 | "node": ">=18"
68 | },
69 | "optionalDependencies": {
70 | "fsevents": "2.3.2"
71 | }
72 | },
73 | "node_modules/playwright-core": {
74 | "version": "1.55.0",
75 | "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.0.tgz",
76 | "integrity": "sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg==",
77 | "dev": true,
78 | "license": "Apache-2.0",
79 | "bin": {
80 | "playwright-core": "cli.js"
81 | },
82 | "engines": {
83 | "node": ">=18"
84 | }
85 | },
86 | "node_modules/undici-types": {
87 | "version": "7.12.0",
88 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz",
89 | "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==",
90 | "dev": true,
91 | "license": "MIT"
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/example/react-interop/src/react_interop/interop.cljs:
--------------------------------------------------------------------------------
1 | (ns react-interop.interop
2 | (:require [signaali.reactive :as sr]
3 | [reagent.core :as r]
4 | [vrac.web :as vw]
5 | [uix.core :as uix]
6 | [uix.dom :as dom]))
7 |
8 | ;; ---------------------------------------------------
9 | ;; Interop utility function
10 |
11 | ;; Signaali effect which mounts some React vdom on a dom node.
12 | (defn mount-react-root-effect [dom-node-ref react-vdom-fn]
13 | (let [create-root-effect (sr/create-effect
14 | (fn []
15 | (when-some [dom-node @dom-node-ref]
16 | (let [^js react-root (dom/create-root dom-node)]
17 | (sr/on-clean-up (fn [] (dom/unmount-root react-root)))
18 | ;; This value is returned when this effect is deref'd
19 | react-root))))
20 | re-render-effect (sr/create-effect
21 | (fn []
22 | (when-some [react-root @create-root-effect]
23 | (dom/render-root (react-vdom-fn) react-root))))]
24 | (vw/use-effects [create-root-effect re-render-effect])))
25 |
26 |
27 | ;; ---------------------------------------------------
28 | ;; Siagent
29 |
30 | ;; React component implemented using Siagent
31 | ;; It demonstrates the interop with the reactive node from Signaali:
32 | ;; Basically no interop is needed, Siagent is taking care of it.
33 | (defn my-reagent-component [text text-in-reactive-node]
34 | [:div
35 | [:div "React function's input: \"" text "\""]
36 | [:div "Signaali reactive node: \"" @text-in-reactive-node "\""]])
37 |
38 | ;; Vrac component
39 | (defn interop-with-reagent-component-article [text1 text2]
40 | (vw/$ :article
41 | (vw/$ :h2 "React component implemented via Siagent")
42 | (let [dom-node-ref (sr/create-signal nil)]
43 | (vw/$ :div {:ref dom-node-ref}
44 | (mount-react-root-effect dom-node-ref
45 | (fn []
46 | (r/as-element [my-reagent-component @text1 text2])))))))
47 |
48 | ;; ---------------------------------------------------
49 | ;; UIx
50 |
51 | ;; React component implemented using UIx
52 | ;; It demonstrates the interop with the reactive node from Signaali:
53 | ;; You need to use the `use-reactive` React hook.
54 | (uix/defui my-react-component [{:keys [text text-in-reactive-node]}]
55 | (uix/$ :div
56 | (uix/$ :div "React function's input: \"" text "\"")
57 | (uix/$ :div "Signaali reactive node: \"" (r/use-reactive text-in-reactive-node) "\"")))
58 |
59 | ;; Vrac component
60 | (defn interop-with-uix-component-article [text1 text2]
61 | (vw/$ :article
62 | (vw/$ :h2 "React component implemented via UIx")
63 | (let [dom-node-ref (sr/create-signal nil)]
64 | (vw/$ :div {:ref dom-node-ref}
65 | (mount-react-root-effect dom-node-ref
66 | (fn []
67 | (uix/$ my-react-component {:text @text1
68 | :text-in-reactive-node text2})))))))
69 |
70 | ;; ---------------------------------------------------
71 |
72 | ;; Vrac component
73 | (defn- text-input [label text-signal]
74 | (vw/$ :div
75 | label ": "
76 | (vw/$ :input
77 | (vw/props-effect (fn [] {:value @text-signal}))
78 | {:on/input (fn [^js event]
79 | (reset! text-signal (-> event .-target .-value)))})))
80 |
81 | ;; Vrac component
82 | (defn interop-demo []
83 | (vw/$ :main
84 | (vw/$ :h1 "Vrac + React interop demo")
85 | (let [text1 (sr/create-signal "text1")
86 | text2 (sr/create-signal "text2")]
87 | (vw/$ :div
88 | (vw/$ text-input "Text1" text1)
89 | (vw/$ text-input "Text2" text2)
90 |
91 | (vw/$ interop-with-reagent-component-article text1 text2)
92 | (vw/$ interop-with-uix-component-article text1 text2)))))
93 |
--------------------------------------------------------------------------------
/example/si-frame-simple/src/simple/core.cljs:
--------------------------------------------------------------------------------
1 | (ns simple.core
2 | (:require [re-frame.core :as rf]
3 | [signaali.reactive :as sr]
4 | [vrac.web :as vw :refer [$]]))
5 |
6 | ;; A detailed walk-through of this source code is provided in the docs:
7 | ;; https://day8.github.io/re-frame/dominoes-live/
8 |
9 | ;; -- Domino 1 - Event Dispatch -----------------------------------------------
10 |
11 | (defn dispatch-timer-event
12 | []
13 | (let [now (js/Date.)]
14 | (rf/dispatch [:timer now]))) ;; <-- dispatch used
15 |
16 | ;; Call the dispatching function every second.
17 | ;; `defonce` is like `def` but it ensures only one instance is ever
18 | ;; created in the face of figwheel hot-reloading of this file.
19 | (defonce do-timer (js/setInterval dispatch-timer-event 1000))
20 |
21 | ;; -- Domino 2 - Event Handlers -----------------------------------------------
22 |
23 | (rf/reg-event-db ;; sets up initial application state
24 | :initialize ;; usage: (dispatch [:initialize])
25 | (fn [_ _] ;; the two parameters are not important here, so use _
26 | {:time (js/Date.) ;; What it returns becomes the new application state
27 | :time-color "orange"})) ;; so the application state will initially be a map with two keys
28 |
29 | (rf/reg-event-db ;; usage: (dispatch [:time-color-change 34562])
30 | :time-color-change ;; dispatched when the user enters a new colour into the UI text field
31 | (fn [db [_ new-color-value]] ;; -db event handlers given 2 parameters: current application state and event (a vector)
32 | (assoc db :time-color new-color-value))) ;; compute and return the new application state
33 |
34 | (rf/reg-event-db ;; usage: (dispatch [:timer a-js-Date])
35 | :timer ;; every second an event of this kind will be dispatched
36 | (fn [db [_ new-time]] ;; note how the 2nd parameter is destructured to obtain the data value
37 | (assoc db :time new-time))) ;; compute and return the new application state
38 |
39 | ;; -- Domino 4 - Query -------------------------------------------------------
40 |
41 | (rf/reg-sub
42 | :time
43 | (fn [db _] ;; db is current app state. 2nd unused param is query vector
44 | (:time db))) ;; return a query computation over the application state
45 |
46 | (rf/reg-sub
47 | :time-color
48 | (fn [db _]
49 | (:time-color db)))
50 |
51 | ;; -- Domino 5 - View Functions ----------------------------------------------
52 |
53 | (defn clock []
54 | (let [colour (rf/subscribe [:time-color])
55 | time (sr/create-derived
56 | (fn []
57 | (-> @(rf/subscribe [:time])
58 | .toTimeString
59 | (clojure.string/split " ")
60 | first)))]
61 | ($ :div.example-clock
62 | (vw/props-effect (fn []
63 | {:style {:color @colour}}))
64 | time)))
65 |
66 | (defn color-input []
67 | ($ :div.color-input
68 | "Display color: "
69 | ($ :input
70 | (vw/props-effect (fn []
71 | {:value @(rf/subscribe [:time-color])}))
72 | {:type "text"
73 | :style {:border "1px solid #CCC"}
74 | :on/input (fn [^js e]
75 | (let [text (-> e .-target .-value)]
76 | (rf/dispatch [:time-color-change text])))})))
77 |
78 | (defn ui []
79 | ($ :div
80 | ($ :h1 "The time is now:")
81 | ($ clock)
82 | ($ color-input)))
83 |
84 | ;; -- Entry Point -------------------------------------------------------------
85 |
86 | (defn mount-ui []
87 | (vw/render (js/document.getElementById "app")
88 | ($ ui)))
89 |
90 | (defn ^:dev/after-load clear-cache-and-render! []
91 | (rf/clear-subscription-cache!)
92 | (mount-ui))
93 |
94 | (defn ^:dev/before-load shutdown! []
95 | (vw/dispose-render-effects))
96 |
97 | (defn run []
98 | (rf/dispatch-sync [:initialize])
99 | (mount-ui))
100 |
--------------------------------------------------------------------------------
/example/test-app/src/example/context.cljs:
--------------------------------------------------------------------------------
1 | (ns example.context
2 | (:require [signaali.reactive :as sr]
3 | [vrac.web :as vw :refer [$]]))
4 |
5 | (def ^:dynamic *diy-context* (sr/create-signal {:a 10}))
6 |
7 | (defn- display-diy-context [label]
8 | (let [context *diy-context*]
9 | ($ :div label ": " context)))
10 |
11 | (defn- diy-context-article []
12 | (let [counter (sr/create-signal 0)]
13 | ($ :article
14 | ($ :h2 "DIY context using dynamic variables")
15 | ($ :button {:on/click #(swap! counter inc)} "counter = " counter)
16 | (display-diy-context "global context")
17 | (let [context *diy-context* ;; The current context, expectedly a reactive node.
18 | local-context (sr/create-derived (fn [] (-> @context
19 | (assoc :counter @counter)
20 | (update :a + @counter))))]
21 | ;; Create a derived inner context, and set it as the new current one within this scope.
22 | (binding [*diy-context* local-context]
23 | ;; Read the new current context from somewhere in the scope.
24 | (display-diy-context "local context")))
25 | (display-diy-context "back to the global context")
26 |
27 | ($ :div
28 | ($ :strong "Warning: ")
29 | "This won't work if we call the inner components using "
30 | ($ :code "($ display-diy-context ,,,)")))))
31 |
32 | (defn- display-built-in-context [label]
33 | ($ :div label ": " (vw/get-context)))
34 |
35 | (defn- builtin-context-article []
36 | (let [root-context (sr/create-signal {:a 10})
37 | counter (sr/create-signal 0)]
38 | (vw/with-context root-context
39 | ($ :article
40 | ($ :h2 "Built-in context")
41 | ($ :button {:on/click #(swap! counter inc)} "counter = " counter)
42 | ($ display-built-in-context "global context")
43 | (vw/with-context-update (fn [parent-context]
44 | (-> @parent-context
45 | (assoc :counter @counter)
46 | (update :a + @counter)))
47 | ($ display-built-in-context "local context"))
48 | ($ display-built-in-context "back to the global context")))))
49 |
50 | (defn- person-component [{:keys [name]}]
51 | ($ :<>
52 | name
53 | (if-some [context (vw/get-context)]
54 | (sr/create-derived (fn []
55 | (when (:birthday-party @context)
56 | " says HAPPY BIRTHDAY !!! \uD83E\uDD73")))
57 | " is there, where is the context ?!")))
58 |
59 | (defn- context-on-reactive-fragment []
60 | (let [root-context (sr/create-signal {:birthday-party true})
61 | person-count (sr/create-signal 2)
62 | persons (sr/create-derived (fn []
63 | (->> (cycle ["Alice" "Bob" "Connie" "Diva" "Elric" "Fred" "Giana"])
64 | (take @person-count)
65 | (map-indexed (fn [index name]
66 | {:id index
67 | :name name})))))]
68 | ($ :article
69 | ($ :h2 "Context on a reactive fragment")
70 | ($ :button {:on/click #(swap! person-count inc)} "Add 1 person to the party")
71 | ($ :button {:on/click #(swap! root-context update :birthday-party not)}
72 | (sr/create-memo (fn []
73 | (if (-> @root-context :birthday-party)
74 | "Turn the party OFF"
75 | "Turn the party ON"))))
76 | (vw/with-context root-context
77 | ($ :ul
78 | (vw/for-fragment* persons :id
79 | (fn [person] ($ :li
80 | ($ person-component person)))))))))
81 |
82 | (defn context-root []
83 | ($ :div
84 | ($ diy-context-article)
85 | ($ builtin-context-article)
86 | ($ context-on-reactive-fragment)
87 | ,))
88 |
--------------------------------------------------------------------------------
/test/vrac/dsl/macro_test.cljc:
--------------------------------------------------------------------------------
1 | (ns vrac.dsl.macro-test
2 | (:require [clojure.test :refer [deftest testing is are]]
3 | [vrac.dsl.macro :as sut]
4 | [vrac.test.util :refer [make-gensym]]))
5 |
6 | (deftest thread-first-test
7 | (testing "the -> macro"
8 | (is (= `(prn (+ ~'a 1))
9 | (sut/thread-first `(-> ~'a (+ 1) prn))))))
10 |
11 | (deftest thread-last-test
12 | (testing "the ->> macro"
13 | (is (= `(prn (+ 1 ~'a))
14 | (sut/thread-last `(->> ~'a (+ 1) prn))))))
15 |
16 | (deftest thread-as-test
17 | (testing "the as-> macro"
18 | (is (= `(let [~'y (+ 1 2)
19 | ~'y (+ ~'y 3)]
20 | (+ ~'y 4))
21 | (sut/thread-as `(as-> (+ 1 2) ~'y
22 | (+ ~'y 3)
23 | (+ ~'y 4)))))))
24 |
25 | (deftest destruct-test
26 | (testing "sequential destruct"
27 | (is (= `[~'d ~'x
28 | ~'a (nth ~'d 0)
29 | ~'b (nth ~'d 1)
30 | ~'c (nth ~'d 2)
31 | ~'rest (drop 3 ~'d)]
32 | (#'sut/destructure '[[a b c & rest :as d] x]))))
33 |
34 | (testing "associative destruct"
35 | (is (= '[x y
36 | a (:aa x)
37 | bb (:bb x)
38 | c (:cc bb)
39 | d (:dd bb)]
40 | (#'sut/destructure '[{a :aa
41 | {c :cc
42 | d :dd
43 | :as bb} :bb
44 | :as x} y]))))
45 |
46 | (testing "associative destruct inside sequential destruct"
47 | (with-redefs [gensym (make-gensym)]
48 | (is (= `[~'x ~'y
49 | ~'map__1 (nth ~'x 0)
50 | ~'a (:a ~'map__1)
51 | ~'map__2 (nth ~'x 1)
52 | ~'b (:b ~'map__2)]
53 | (#'sut/destructure '[[{a :a} {b :b} :as x] y])))))
54 |
55 | (testing "sequential destruct inside associative destruct"
56 | (with-redefs [gensym (make-gensym)]
57 | (is (= `[~'x ~'y
58 | ~'vec__1 (:aa ~'x)
59 | ~'a1 (nth ~'vec__1 0)
60 | ~'a2 (nth ~'vec__1 1)
61 | ~'bb (:bb ~'x)
62 | ~'vec__2 (:cc ~'bb)
63 | ~'c1 (nth ~'vec__2 0)
64 | ~'c2 (nth ~'vec__2 1)]
65 | (#'sut/destructure '[{[a1 a2] :aa
66 | {[c1 c2] :cc
67 | :as bb} :bb
68 | :as x} y])))))
69 |
70 | (testing "default values inside associative destruct"
71 | (is (= '[x y
72 | a (:aa x)
73 | b (:bb x :bb-default)
74 | i (:i x)
75 | j (:j x :jj-default)
76 | k (:foo/k x)
77 | l (:foo/l x :ll-default)
78 | p (:bar/p x)
79 | q (:bar/q x :qq-default)
80 | r (:foo/r x)
81 | s (:foo/s x :ss-default)]
82 | (#'sut/destructure '[{a :aa
83 | b :bb
84 | :keys [i j foo/k foo/l]
85 | :bar/keys [p q foo/r foo/s]
86 | :or {b :bb-default
87 | j :jj-default
88 | l :ll-default
89 | q :qq-default
90 | s :ss-default}
91 | :as x} y]))))
92 |
93 |
94 | (testing "the :& syntax in associative destruct"
95 | (is (= `[~'x ~'y
96 | ~'a (:a ~'x)
97 | ~'b (:b ~'x)
98 | ~'c (:c ~'x)
99 | ~'bar (:foo/bar ~'x)
100 | ~'baz (:foo/baz ~'x)
101 | ~'rest (dissoc ~'x :a :b :c :foo/bar :foo/baz)]
102 | (#'sut/destructure '[{a :a
103 | :keys [b c]
104 | :foo/keys [bar baz]
105 | :& rest
106 | :as x} y])))))
107 |
108 | (deftest expand-let-test
109 | (testing "let bindings expansion"
110 | (with-redefs [gensym (make-gensym)]
111 | (is (= `(~'let [~'map__1 {:a 1}
112 | ~'a (:a ~'map__1)]
113 | ~'a)
114 | (sut/expand-let `(~'let [~'{a :a} {:a 1}] ~'a)))))
115 | (with-redefs [gensym (make-gensym)]
116 | (is (= `(~'let [~'a 1
117 | ~'b 2]
118 | (~'do
119 | ~'a
120 | ~'b))
121 | (sut/expand-let `(~'let [~'a 1
122 | ~'b 2]
123 | ~'a
124 | ~'b)))))))
125 |
126 | (deftest expand-for-test
127 | (testing "for bindings expansion"
128 | (with-redefs [gensym (make-gensym)]
129 | (is (= `(~'for [~'item__1 [[1 2] [3 4]]
130 | :let [~'vec__2 ~'item__1
131 | ~'a (nth ~'vec__2 0)
132 | ~'b (nth ~'vec__2 1)]
133 | :let [~'vec__3 [10 20]
134 | ~'c (nth ~'vec__3 0)
135 | ~'d (nth ~'vec__3 1)]]
136 | [~'a ~'b ~'c ~'d])
137 | (sut/expand-for `(~'for [~'[a b] ~[[1 2] [3 4]]
138 | :let [~'[c d] ~'[10 20]]]
139 | ~'[a b c d])))))))
140 |
141 | (deftest expand-when-test
142 | (testing "when single body"
143 | (is (= '(when true (do 1 2))
144 | (sut/expand-when '(when true 1 2))))))
145 |
146 | (deftest expand-fn-test
147 | (testing "fn single body"
148 | (is (= '(fn [a] (do 1 2))
149 | (sut/expand-fn '(fn [a] 1 2))))
150 | (is (= '(fn foo [a] (do 1 2))
151 | (sut/expand-fn '(fn foo [a] 1 2))))))
152 |
153 | (deftest expand-defn-test
154 | (testing "defn single body"
155 | (is (= '(defn foo [a] (do 1 2))
156 | (sut/expand-defn '(defn foo [a] 1 2))))))
157 |
--------------------------------------------------------------------------------
/example/test-app/src/example/svg.cljs:
--------------------------------------------------------------------------------
1 | (ns example.svg
2 | (:require [shadow.resource :as rc]
3 | [vrac.web :as vw :refer [$]]))
4 |
5 | (defn- fox-origami []
6 | ;; TODO: Should I cache the dom and provide a clone on each usage?
7 | ;; Is there another way to reference the same SVG from multiple places?
8 | ;; Is the DOM in fact a directed graph? What happens if I add the same DOM element in multiple places?
9 | (vw/html-text-to-dom (rc/inline "svg/fox-origami.svg")))
10 |
11 | (defn- string-to-svg-element []
12 | ($ :article
13 | ($ :h2 "Using an SVG element built from a string")
14 | (fox-origami)))
15 |
16 | (defn- changing-an-existing-svg-element []
17 | ($ :article
18 | ($ :h2 "Changing an existing SVG element")
19 |
20 | ($ :h3 "Custom width and height")
21 | ($ (fox-origami)
22 | {:width "20rem"
23 | :height "20rem"})
24 |
25 | ($ :h3 "Then add children")
26 | ($ (fox-origami)
27 | {:width "20rem"
28 | :height "20rem"}
29 | ;; Right eye
30 | ($ :circle {:cx 14.5
31 | :cy 20
32 | :r 3.5
33 | :fill "white"
34 | :stroke "black"
35 | :stroke-width 1})
36 | ;; Left eye
37 | ($ :circle {:cx 24.5
38 | :cy 20
39 | :r 3.5
40 | :fill "white"
41 | :stroke "black"
42 | :stroke-width 1}))))
43 |
44 | (defn- svg-element-using-vcup []
45 | ($ :article
46 | ($ :h2 "SVG element using Vcup")
47 |
48 | ;; from https://www.svgrepo.com/svg/423821/fox-origami-paper
49 | ;; Author: Lima Studio
50 | ;; License: CC Attribution License
51 | ($ :svg {:width "800px"
52 | :height "800px"
53 | :viewBox "0 0 64 64"
54 | :xmlns "http://www.w3.org/2000/svg"}
55 | ($ :path {:d "M7.45,21.19l9.65-5.88L7.56,5.16c-.16,.12-.27,.31-.27,.54v14.64c0,.29,.06,.58,.16,.85h0Z"
56 | :fill "#fca65c"})
57 | ($ :path {:d "M31.17,5.16l-9.54,10.15,9.81,6.04V5.69c0-.22-.11-.41-.27-.53Z"
58 | :fill "#fca65c"})
59 | ($ :path {:d "M7.56,5.16l9.21,9.8-1.95,1.75-3.16,1.94L7.56,5.16Z"
60 | :fill "#f5934a"})
61 | ($ :path {:d "M31.17,5.16l-9.21,9.8,1.95,1.75,3.16,1.94,4.1-13.5Z"
62 | :fill "#f5934a"})
63 | ($ :path {:d "M8.05,5.01c-.18-.02-.35,.04-.49,.15l5.77,10.15h12.07l5.77-10.15c-.18-.15-.43-.21-.67-.11l-11.13,4.22L8.23,5.05c-.06-.02-.12-.04-.18-.04h0Z"
64 | :fill "#feb87e"})
65 | ($ :path {:d "M12.78,26.84v26.44c0,.26,.05,.52,.14,.77l13.81-10.57,4.71-22.13-18.65,5.49Z"
66 | :fill "#f0f4f6"})
67 | ($ :path {:d "M31.44,21.35L12.78,44.91V26.84l18.65-5.49Z"
68 | :fill "#e2eef2"})
69 | ($ :path {:d "M31.44,21.34L12.92,54.04c.12,.35,.31,.68,.58,.95l3.3,3.3c.46,.46,1.07,.71,1.72,.71h13.49l19.14-7.05,1.15-16.09c-.23-.27-.49-.51-.78-.71l-20.07-13.81Z"
70 | :fill "#fca65c"})
71 | ($ :path {:d "M49.88,34.03l-5.27,14.03-18.3,10.94h5.69l19.14-7.05,1.15-16.09c-.23-.27-.49-.51-.78-.71l-1.63-1.12Z"
72 | :fill "#f5934a"})
73 | ($ :path {:d "M52.29,35.86l-4.72,13.71,6.76-.99,2.27-4.48c-.08-.33-.2-.65-.36-.96l-3.44-6.53c-.14-.27-.32-.52-.52-.75Z"
74 | :fill "#feb87e"})
75 | ($ :path {:d "M25.4,15.31l-6.04-6.03-6.03,6.03h0l-5.88,5.88c.12,.32,.3,.62,.55,.87l4.78,4.78,1.23,1.23,5.16,1.6,5.54-1.6h0l6.72-6.72-6.04-6.04Z"
76 | :fill "#fdc99c"})
77 | ($ :path {:d "M14.01,28.07l3.74,3.74c.89,.89,2.33,.89,3.22,0l3.74-3.74H14.01Z"
78 | :fill "#9dacb9"})
79 | ($ :path {:d "M56.6,44.1l-9.03,5.47h0l-15.57,9.43h23.7c.56,0,1.01-.45,1.01-1.01v-12.96c0-.32-.04-.63-.11-.94h0Z"
80 | :fill "#fdc99c"})
81 | ($ :path {:d "M19.36,9.27l-7.16,4.07,1.12,1.97,6.03-6.04Z"
82 | :fill "#fca65c"})
83 | ($ :path {:d "M19.36,9.27l7.16,4.07-1.12,1.97-6.03-6.04Z"
84 | :fill "#fca65c"})
85 | ($ :path {:d "M56.6,44.1l-7.64,1.43-1.39,4.04,9.03-5.47Z"
86 | :fill "#fca65c"})
87 | ($ :path {:d "M12.79,55.7l3.3,3.3c.65,.65,1.51,1,2.42,1H55.7c1.11,0,2.01-.9,2.01-2.01v-12.96c0-.82-.2-1.63-.58-2.36l-3.43-6.53c-.38-.72-.94-1.35-1.62-1.82l-19.64-13.51V5.69c0-.56-.27-1.08-.73-1.39-.46-.32-1.04-.39-1.56-.19l-10.78,4.09L8.58,4.11c-.52-.2-1.1-.13-1.56,.19s-.73,.84-.73,1.39v14.64c0,.92,.36,1.78,1,2.42l4.49,4.49v26.02c0,.92,.36,1.78,1,2.42Zm1-26.45l3.26,3.26c1.28,1.28,3.36,1.28,4.63,0l5.26-5.26-13.16,23.23V29.25Zm2.65-.18h5.87l-2.03,2.03c-.5,.5-1.31,.5-1.81,0l-2.03-2.03Zm19.15,28.93l20.13-12.19v12.19h-20.13Zm19.77-14.39s0,.05,.02,.07l-5.96,3.61,3.11-9.05,2.83,5.37Zm-4.41-7.63c.08,.05,.11,.14,.18,.2l-4.38,12.72-15.02,9.1h-13.21c-.38,0-.74-.15-1.01-.42l-3.3-3.3c-.07-.07-.06-.19-.11-.28L31.77,22.79l19.17,13.19ZM12.08,15.14l-3.79,3.79V8.47l3.79,6.67Zm-2.55-8.53l8.03,3.05-4.02,4.02-4.02-7.06Zm20.9,1.86v10.46l-3.79-3.79,3.79-6.67Zm-5.26,5.2l-4.02-4.02,8.03-3.05-4.02,7.07Zm-16.47,7.67h0l10.66-10.66,10.66,10.66-5.72,5.72H14.43l-5.72-5.72Z"}))))
88 |
89 | (defn- svg-element-with-class-and-style []
90 | ($ :article
91 | ($ :h2 "SVG element with a class and style")
92 | ($ :div
93 | ($ :svg {:xmlns "http://www.w3.org/2000/svg"
94 | :class "my-class"
95 | :style {:color "lightgreen"}}
96 | ($ :circle {:cx 50
97 | :cy 50
98 | :r 50
99 | :fill "currentColor"})))))
100 |
101 | (defn- foreign-object-inside-svg []
102 | ($ :article
103 | ($ :h2 " element inside SVG")
104 | ($ :svg {:xmlns "http://www.w3.org/2000/svg"
105 | :width "40rem"
106 | :height "40rem"
107 | :viewBox "0 0 200 200"}
108 | ($ :polygon {:points "5,5 195,10 185,185 10,195"})
109 | ($ :foreignObject {:x "20"
110 | :y "20"
111 | :width "160"
112 | :height "160"}
113 | ($ :div {:xmlns "http://www.w3.org/1999/xhtml"
114 | :style {:background-color "purple"
115 | :color "pink"
116 | :height "100%"}}
117 | "This is an HTML div inside a SVG element. "
118 | "It is wrapped inside a 'foreignObject' SVG element.")))))
119 |
120 | (defn svg-root []
121 | ($ :div
122 | ($ string-to-svg-element)
123 | ($ changing-an-existing-svg-element)
124 | ($ svg-element-using-vcup)
125 | ($ svg-element-with-class-and-style)
126 | ($ foreign-object-inside-svg)
127 | ,))
128 |
--------------------------------------------------------------------------------
/doc/ai-context.md:
--------------------------------------------------------------------------------
1 | # Vrac + Signaali + Si-Frame Code Generation Guide
2 |
3 | ## Dependencies
4 | ```clojure
5 | taipei.404.vrac/vrac {:mvn/version "0.1.2"}
6 | ```
7 |
8 | ## Namespace Setup
9 | ```clojure
10 | (ns example.app
11 | (:require [vrac.web :as vw :refer [$]]
12 | [signaali.reactive :as sr]
13 | [re-frame.core :as rf])) ; Optional, for Si-Frame integration
14 | ```
15 |
16 | ## Element Creation with `$`
17 | - **HTML elements**: `($ :div ...)` `($ :span#id.class1.class2 ...)`
18 | - **Fragments**: `($ :<> child1 child2 ...)`
19 | - **Components**: `($ component-fn arg1 arg2)`
20 | - **Children**: strings, numbers, booleans, keywords, vectors, nil (renders nothing), reactive nodes
21 |
22 | ## Properties (Props)
23 |
24 | ### Static Props
25 | ```clojure
26 | ($ :div.class1 {:style {:color "red" :padding "1em"}
27 | :class [:class2]
28 | :data-testid "my-div"}
29 | "content")
30 | ```
31 |
32 | ### Reactive Props
33 | Use `vw/props-effect` with a function returning a prop map:
34 | ```clojure
35 | ($ :input
36 | (vw/props-effect (fn [] {:value @text-signal}))
37 | {:type "text"
38 | :on/input (fn [e] (reset! text-signal (.. e -target -value)))})
39 | ```
40 |
41 | ### Prop Namespaces
42 | - `:a/...` - HTML attributes (e.g., `:a/for`, `:a/readonly`)
43 | - `:p/...` - DOM properties (e.g., `:p/htmlFor`, `:p/innerHTML`)
44 | - `:on/...` - Event handlers (e.g., `:on/click`, `:on/input`)
45 | - No namespace - Vrac special props (`:style`, `:class`, `:ref`, `:data-*`) or DOM properties
46 |
47 | ### Multiple Prop Maps
48 | Props compose together; later values override earlier ones (except `:style` and `:class` which merge):
49 | ```clojure
50 | ($ :div {:class :base-class}
51 | {:class :extra-class} ; Results in both classes
52 | {:style {:color "red"}}
53 | {:style {:padding "1em"}}) ; Both styles applied
54 | ```
55 |
56 | ## Reactive State (Signaali)
57 |
58 | ### Creating State
59 | ```clojure
60 | (let [counter (sr/create-state 0) ; Mutable state
61 | text (sr/create-signal "hello") ; Immutable signal
62 | doubled (sr/create-derived (fn [] (* 2 @counter))) ; Computed
63 | memoized (sr/create-memo (fn [] (expensive-calc @counter)))] ; Cached computed
64 | ...)
65 | ```
66 |
67 | ### Using State
68 | - Read: `@signal-or-state`
69 | - Write: `(reset! signal-or-state new-value)` or `(swap! signal-or-state update-fn)`
70 | - Reactive nodes auto-update UI when dereferenced in render
71 |
72 | ## Reactive Fragments
73 |
74 | Don't use reactive fragment on static data
75 |
76 | ### Conditional Rendering
77 | ```clojure
78 | ;; If/else
79 | (vw/if-fragment condition-fn
80 | ($ :div "then branch")
81 | ($ :div "else branch"))
82 |
83 | ;; When (no else)
84 | (vw/when-fragment condition-fn
85 | ($ :div "shown when true"))
86 |
87 | ;; Case
88 | (vw/case-fragment value-fn
89 | :route/home ($ :div "Home")
90 | :route/blog ($ :div "Blog")
91 | ($ :div "Default"))
92 |
93 | ;; Cond
94 | (vw/cond-fragment
95 | (= @route :home) ($ :div "Home")
96 | (= @route :blog) ($ :div "Blog")
97 | :else ($ :div "Default"))
98 | ```
99 |
100 | **Important**: Pass a **function** to `vw/if-fragment`, `vw/when-fragment`, and `vw/case-fragment`:
101 | ```clojure
102 | ;; Correct:
103 | (vw/if-fragment (fn [] (even? @counter)) ...)
104 | (vw/if-fragment counter ...) ; OK if counter is already a reactive node
105 |
106 | ;; Wrong:
107 | (vw/if-fragment (even? @counter) ...) ; Don't evaluate directly
108 | ```
109 |
110 | ### List Rendering
111 | ```clojure
112 | (vw/for-fragment*
113 | coll-fn ; (fn [] @items) or just items-signal
114 | key-fn ; :id or (fn [item] (:id item)), optional
115 | item-component) ; (fn [item] ($ :div (:name item)))
116 | ```
117 |
118 | ## Components
119 | Define as functions returning vcup:
120 | ```clojure
121 | (defn person-card [person]
122 | ($ :div.card
123 | ($ :h3 (:name person))
124 | ($ :p (:bio person))))
125 |
126 | ;; Use with $
127 | ($ person-card {:name "Alice" :bio "Developer"})
128 | ```
129 |
130 | ## Context
131 | ```clojure
132 | ;; Set context
133 | (vw/with-context (sr/create-signal {:theme "dark"})
134 | ($ child-component))
135 |
136 | ;; Update based on parent context
137 | (vw/with-context-update (fn [parent-ctx]
138 | (assoc @parent-ctx :nested true))
139 | ($ child-component))
140 |
141 | ;; Read context
142 | (defn child-component []
143 | (let [ctx (vw/get-context)]
144 | ($ :div "Theme: " (sr/create-derived (fn [] (:theme @ctx))))))
145 | ```
146 |
147 | ## Refs
148 | Capture DOM elements:
149 | ```clojure
150 | (let [element-ref (sr/create-signal nil)]
151 | ($ :div
152 | ($ :input {:ref element-ref})
153 | ($ :button {:on/click (fn [] (.focus @element-ref))} "Focus input")))
154 | ```
155 |
156 | ## Re-frame Integration
157 | ```clojure
158 | ;; Subscriptions
159 | (rf/reg-sub :counter (fn [db _] (:counter db)))
160 |
161 | ;; In components - wrap subscriptions for derived values
162 | (defn counter-display []
163 | (let [counter (rf/subscribe [:counter])
164 | doubled (sr/create-derived (fn [] (* 2 @counter)))]
165 | ($ :div "Counter: " counter " Doubled: " doubled)))
166 |
167 | ;; Dispatch events
168 | ($ :button {:on/click #(rf/dispatch [:increment])} "+")
169 | ```
170 |
171 | ## Application Entry Point
172 | ```clojure
173 | (defn mount-ui []
174 | (vw/render (js/document.getElementById "app")
175 | ($ root-component)))
176 |
177 | (defn ^:dev/after-load reload! []
178 | (mount-ui))
179 |
180 | (defn ^:dev/before-load shutdown! []
181 | (vw/dispose-render-effects))
182 |
183 | (defn init []
184 | (mount-ui))
185 | ```
186 |
187 | ## SVG and MathML
188 | Use standard element names:
189 | ```clojure
190 | ;; SVG
191 | ($ :svg {:width "100" :height "100" :xmlns "http://www.w3.org/2000/svg"}
192 | ($ :circle {:cx 50 :cy 50 :r 40 :fill "blue"}))
193 |
194 | ;; MathML
195 | ($ :math {:display "block"}
196 | ($ :mrow
197 | ($ :mi "x")
198 | ($ :mo "+")
199 | ($ :mn "1")))
200 |
201 | ;; Convert HTML/SVG string to DOM
202 | (vw/html-text-to-dom "")
203 | ```
204 |
205 | ## Common Patterns
206 |
207 | ### Counter
208 | ```clojure
209 | (defn counter []
210 | (let [count (sr/create-state 0)]
211 | ($ :div
212 | "Count: " count
213 | ($ :button {:on/click #(swap! count inc)} "+"))))
214 | ```
215 |
216 | ### Controlled Input
217 | ```clojure
218 | (defn text-input []
219 | (let [value (sr/create-signal "")]
220 | ($ :input
221 | (vw/props-effect (fn [] {:value @value}))
222 | {:on/input (fn [e] (reset! value (.. e -target -value)))})))
223 | ```
224 |
225 | ### Toggle Visibility
226 | ```clojure
227 | (defn collapsible [title content]
228 | (let [open? (sr/create-state false)]
229 | ($ :div
230 | ($ :button {:on/click #(swap! open? not)} title)
231 | (vw/when-fragment open?
232 | ($ :div content)))))
233 | ```
--------------------------------------------------------------------------------
/example/test-app/src/example/reactive_fragment.cljs:
--------------------------------------------------------------------------------
1 | (ns example.reactive-fragment
2 | (:require [clojure.string :as str]
3 | [signaali.reactive :as sr]
4 | [vrac.web :as vw :refer [$]]))
5 |
6 | (defn- counter-component [counter-state]
7 | ($ :div
8 | "Counter value: " counter-state
9 | ($ :div
10 | ($ :button {:on/click #(swap! counter-state inc)} "+1")
11 | ($ :button {:on/click #(swap! counter-state (fn [n] (+ n 2)))} "+2"))))
12 |
13 | (defn- if-fragment-article []
14 | (let [counter-state (sr/create-state 0)]
15 | ($ :article
16 | ($ :h2 "If fragment")
17 | ($ counter-component counter-state)
18 | (vw/if-fragment (fn [] (even? @counter-state))
19 | ($ :div "The value is even.")
20 | ($ :div "The value is odd.")))))
21 |
22 | (defn- case-fragment-article []
23 | (let [state (sr/create-state {:current-route :route/homepage})]
24 | ($ :article
25 | ($ :h2 "Case fragment")
26 | ($ :button {:on/click #(swap! state assoc :current-route :route/homepage)} "Home")
27 | ($ :button {:on/click #(swap! state assoc :current-route :route/blog)} "Blog")
28 | ($ :button {:on/click #(swap! state assoc :current-route :route/about)} "About")
29 | ($ :button {:on/click #(swap! state assoc :current-route :route/bookmark)} "Non-existing route")
30 | (vw/case-fragment (fn [] (:current-route @state))
31 | :route/homepage ($ :div "This is the homepage.")
32 | :route/blog ($ :div "This is the blog.")
33 | :route/about ($ :div "This is the about page.")
34 | ($ :div "This is the 'not found' page, for any other route.")))))
35 |
36 | (defn- cond-fragment-article []
37 | (let [current-route (sr/create-state :route/homepage)]
38 | ($ :article
39 | ($ :h2 "Cond fragment")
40 | ($ :button {:on/click #(reset! current-route :route/homepage)} "Home")
41 | ($ :button {:on/click #(reset! current-route :route/blog)} "Blog")
42 | ($ :button {:on/click #(reset! current-route :route/about)} "About")
43 | ($ :button {:on/click #(reset! current-route :route/bookmark)} "Non-existing route")
44 | ;; The vw/cond-fragment macro groups the clauses' conditions and wraps them into a fn.
45 | (vw/cond-fragment
46 | (= @current-route :route/homepage) ($ :div "This is the homepage.")
47 | (= @current-route :route/blog) ($ :div "This is the blog.")
48 | (= @current-route :route/about) ($ :div "This is the about page.")
49 | :else ($ :div "This is the 'not found' page, for any other route.")))))
50 |
51 | (defn- for-fragment-article []
52 | (let [state (sr/create-state {:persons [{:id 0 :name "Alice"}
53 | {:id 1 :name "Barbara"}
54 | {:id 2 :name "Cedric"}]})]
55 | ($ :article
56 | ($ :h2 "For fragment")
57 |
58 | (let [new-person-name (sr/create-state "Louise")]
59 | ($ :div
60 | ($ :input
61 | (vw/props-effect (fn [] {:value @new-person-name}))
62 | {:on/input (fn [event]
63 | (reset! new-person-name (-> event .-target .-value)))})
64 | ($ :button {:on/click (fn []
65 | (when-not (str/blank? @new-person-name)
66 | (swap! state update :persons
67 | (fn [persons]
68 | (-> persons
69 | (conj {:id (count persons)
70 | :name @new-person-name})))))
71 | (reset! new-person-name ""))}
72 | "Add")))
73 |
74 | ($ :div
75 | (vw/for-fragment* (fn [] (:persons @state)) ;; coll-fn
76 | :id ;; key-fn
77 | (fn [{:keys [id name]}] ;; item-component
78 | ($ :div "[" id "] " name)))
79 | #_ ;; ideally
80 | (vw/for-fragment [{:keys [id name]} (:persons @state)]
81 | ^{:key id} ($ :div "[" id "] " name))
82 |
83 | #_ ;; nice to have: supports the Clojure for syntax
84 | (vw/for-fragment [person (:persons @state)
85 | :let [{:keys [id name]} person]
86 | :when (even? id)
87 | :while (< id 10)]
88 | ^{:key id} ($ :div "[" id "] " name))
89 | #_ ;; That would translate to this:
90 | (vw/for-fragment* (fn []
91 | (for [person (:persons @state)
92 | :let [{:keys [id name]} person]
93 | :when (even? id)
94 | :while (< id 10)]
95 | ;; Collects everything which is used in the body of the `for`,
96 | ;; either for the key-fn or the item-component
97 | {:id id
98 | :name name}))
99 | ;; Collects everything used in the key expression, here only `id`.
100 | (fn [{:keys [id]}]
101 | id)
102 | ;; Collects everything used in the item, here `id` and `name`.
103 | (fn [{:keys [id name]}]
104 | ($ :div "[" id "] " name)))
105 |
106 | ;; Idea: write some unit tests for the for-fragment macro
107 | ,))))
108 |
109 | (defn dynamic-fragment-crash-test []
110 | ($ :article
111 | ($ :h2 "Dynamic fragments composition (crash test)")
112 | (vw/for-fragment* (range 2)
113 | (fn [index1]
114 | (vw/for-fragment* (range 2)
115 | (fn [index2]
116 | (let [just-started (sr/create-state true)
117 | is-even (sr/create-state true)]
118 | (vw/if-fragment just-started
119 | ($ :div ($ :button {:on/click #(reset! just-started false)} "Start"))
120 | (vw/if-fragment is-even
121 | ($ :div
122 | (str index1 " " index2 " ")
123 | "The value is even."
124 | ($ :button {:on/click #(reset! is-even false)} "Make it odd."))
125 | ($ :div
126 | (str index1 " " index2 " ")
127 | "The value is odd."
128 | ($ :button {:on/click #(reset! is-even true)} "Make it even.")))))))))))
129 |
130 | (defn reactive-fragment-root []
131 | ($ :div
132 | ($ if-fragment-article)
133 | ($ case-fragment-article)
134 | ($ cond-fragment-article)
135 | ($ for-fragment-article)
136 | ($ dynamic-fragment-crash-test)
137 | ,))
138 |
--------------------------------------------------------------------------------
/example/todomvc/src/todomvc/view.cljs:
--------------------------------------------------------------------------------
1 | (ns todomvc.view
2 | (:require [clojure.string :as str]
3 | [re-frame.core :as rf]
4 | [reitit.frontend.easy :as rtfe]
5 | [todomvc.constant :as const]
6 | [signaali.reactive :as sr]
7 | [vrac.web :as vw :refer [$]]))
8 |
9 | (defn- focus-on-create [ref]
10 | (sr/create-effect
11 | (fn []
12 | (when-some [^js input-element @ref]
13 | (.focus input-element)))))
14 |
15 | (defn todo-item-creation-input []
16 | (let [title (rf/subscribe [:comp.input/title])
17 | ref (sr/create-signal nil)]
18 | ($ :<>
19 | (vw/use-effects [(focus-on-create ref)])
20 | ($ :input.new-todo
21 | (vw/props-effect
22 | (fn []
23 | {:value @title}))
24 | {:ref ref
25 | :type "text"
26 | :placeholder "What needs to be done?"
27 | :on/input (fn [^js event]
28 | (rf/dispatch [:comp.input/on-title-changed (-> event .-target .-value)]))
29 | :on/keydown (fn [^js event]
30 | (let [key-pressed (.-which event)
31 | trimmed-title (str/trim @title)]
32 | (when (and (= key-pressed const/enter-keycode)
33 | (not (str/blank? trimmed-title)))
34 | (rf/dispatch [:comp.input/add-todo-item trimmed-title]))))}))))
35 |
36 | (defn toggle-items-button []
37 | (let [all-completed (rf/subscribe [:all-todo-items-completed])]
38 | ($ :<>
39 | ($ :input#toggle-all.toggle-all
40 | {:type "checkbox"
41 | :on/change (fn []
42 | (rf/dispatch [:toggle-all-todo-items @all-completed]))}
43 | (vw/props-effect
44 | (fn []
45 | {:checked @all-completed})))
46 | ($ :label {:htmlFor "toggle-all"} "Mark all as complete"))))
47 |
48 | (defn todo-edit [todo-item editing]
49 | (let [ref (sr/create-signal nil)
50 | default (:title @todo-item)
51 | edit-title (sr/create-state default)]
52 | (vw/when-fragment editing
53 | ($ :<>
54 | (vw/use-effects [(focus-on-create ref)])
55 | ($ :input.edit
56 | (vw/props-effect
57 | (fn []
58 | {:value @edit-title}))
59 | {:ref ref
60 | :type "text"
61 | :on/input (fn [^js event]
62 | (reset! edit-title (-> event .-target .-value)))
63 | :on/blur (fn [_]
64 | (reset! editing false)
65 | (rf/dispatch [:set-todo-item-title (:id @todo-item) @edit-title]))
66 | :on/keydown (fn [^js event]
67 | (let [key-pressed (.-which event)]
68 | (cond
69 | (= key-pressed const/enter-keycode)
70 | (do (reset! editing false)
71 | (rf/dispatch [:set-todo-item-title (:id @todo-item) @edit-title]))
72 |
73 | (= key-pressed const/escape-keycode)
74 | (do (reset! editing false)
75 | (reset! edit-title default)))))})))))
76 |
77 | (defn todo-item [todo-item-id]
78 | (let [editing (sr/create-state false)
79 | todo (rf/subscribe [:todo-item todo-item-id])
80 | show-todo-item (rf/subscribe [:show-todo-item todo-item-id])
81 | title (sr/create-memo (fn [] (:title @todo)))
82 | completed (sr/create-memo (fn [] (:completed @todo)))]
83 | ($ :li
84 | (vw/props-effect
85 | (fn []
86 | {:class [(when @completed "completed")
87 | (when @editing "editing")]
88 | :style {:display (if @show-todo-item
89 | "list-item"
90 | "none")}}))
91 | ($ :div.view
92 | ($ :input.toggle
93 | {:type "checkbox"
94 | :on/change (fn [_]
95 | (rf/dispatch [:toggle-todo-item todo-item-id]))}
96 | (vw/props-effect
97 | (fn []
98 | {:checked @completed})))
99 | ($ :label {:on/dblclick (fn [_]
100 | (reset! editing true))}
101 | title)
102 | ($ :button.destroy {:on/click (fn [_]
103 | (rf/dispatch [:delete-todo-item todo-item-id]))}))
104 | ($ todo-edit todo editing))))
105 |
106 | (defn todo-items-list []
107 | (let [todo-item-ids (rf/subscribe [:todo-item-ids])]
108 | ($ :ul.todo-list
109 | (vw/for-fragment* todo-item-ids identity
110 | (fn [todo-item-id]
111 | ($ todo-item todo-item-id))))))
112 |
113 | (defn todo-items-count []
114 | (let [active-todo-item-count (rf/subscribe [:active-todo-items-count])]
115 | ($ :span.todo-count
116 | ($ :strong
117 | (sr/create-derived (fn []
118 | (str @active-todo-item-count
119 | (if (= @active-todo-item-count 1) " item " " items ")
120 | "left")))))))
121 |
122 | (defn todo-items-filters []
123 | (let [display-type (rf/subscribe [:comp/display-type])]
124 | ($ :ul.filters
125 | ($ :li ($ :a
126 | (vw/props-effect
127 | (fn []
128 | {:class [(when (= @display-type :all) "selected")]}))
129 | {:href (rtfe/href :page/all-todo-items)}
130 | "All"))
131 | ($ :li ($ :a
132 | (vw/props-effect
133 | (fn []
134 | {:class [(when (= @display-type :active) "selected")]}))
135 | {:href (rtfe/href :page/active-todo-items)}
136 | "Active"))
137 | ($ :li ($ :a
138 | (vw/props-effect
139 | (fn []
140 | {:class [(when (= @display-type :completed) "selected")]}))
141 | {:href (rtfe/href :page/completed-todo-items)}
142 | "Completed")))))
143 |
144 | (defn clear-completed-button []
145 | (let [complemented-todo-items-to-clear (rf/subscribe [:complemented-todo-items-to-clear])]
146 | ($ :button.clear-completed
147 | {:on/click (fn [_]
148 | (rf/dispatch [:clean-completed-todo-items]))}
149 | (vw/props-effect
150 | (fn []
151 | {:style {:display (if @complemented-todo-items-to-clear "inline" "none")}}))
152 | "Clear completed")))
153 |
154 | (defn main-page []
155 | (let [any-todo-item (rf/subscribe [:comp.input/any-todo-item])]
156 | ($ :div
157 | ($ :section.todoapp
158 | ($ :header.header
159 | ($ :h1 "todos")
160 | ($ todo-item-creation-input))
161 | ($ :div (vw/props-effect
162 | (fn []
163 | {:style {:display (if @any-todo-item "inline" "none")}}))
164 | ($ :section.main
165 | ($ toggle-items-button)
166 | ($ todo-items-list))
167 | ($ :footer.footer
168 | ($ todo-items-count)
169 | ($ todo-items-filters)
170 | ($ clear-completed-button))))
171 | ($ :footer.info
172 | ($ :p "Double-click to edit a todo")))))
173 |
--------------------------------------------------------------------------------
/example/todomvc/public/css/index.css:
--------------------------------------------------------------------------------
1 | @charset 'utf-8';
2 |
3 | html,
4 | body {
5 | margin: 0;
6 | padding: 0;
7 | }
8 |
9 | button {
10 | margin: 0;
11 | padding: 0;
12 | border: 0;
13 | background: none;
14 | font-size: 100%;
15 | vertical-align: baseline;
16 | font-family: inherit;
17 | font-weight: inherit;
18 | color: inherit;
19 | -webkit-appearance: none;
20 | appearance: none;
21 | -webkit-font-smoothing: antialiased;
22 | -moz-osx-font-smoothing: grayscale;
23 | }
24 |
25 | body {
26 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
27 | line-height: 1.4em;
28 | background: #f5f5f5;
29 | color: #111111;
30 | min-width: 230px;
31 | max-width: 550px;
32 | margin: 0 auto;
33 | -webkit-font-smoothing: antialiased;
34 | -moz-osx-font-smoothing: grayscale;
35 | font-weight: 300;
36 | }
37 |
38 | .hidden {
39 | display: none;
40 | }
41 |
42 | .todoapp {
43 | background: #fff;
44 | margin: 130px 0 40px 0;
45 | position: relative;
46 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
47 | 0 25px 50px 0 rgba(0, 0, 0, 0.1);
48 | }
49 |
50 | .todoapp input::-webkit-input-placeholder {
51 | font-style: italic;
52 | font-weight: 400;
53 | color: rgba(0, 0, 0, 0.4);
54 | }
55 |
56 | .todoapp input::-moz-placeholder {
57 | font-style: italic;
58 | font-weight: 400;
59 | color: rgba(0, 0, 0, 0.4);
60 | }
61 |
62 | .todoapp input::input-placeholder {
63 | font-style: italic;
64 | font-weight: 400;
65 | color: rgba(0, 0, 0, 0.4);
66 | }
67 |
68 | .todoapp h1 {
69 | position: absolute;
70 | top: -140px;
71 | width: 100%;
72 | font-size: 80px;
73 | font-weight: 200;
74 | text-align: center;
75 | color: #b83f45;
76 | -webkit-text-rendering: optimizeLegibility;
77 | -moz-text-rendering: optimizeLegibility;
78 | text-rendering: optimizeLegibility;
79 | }
80 |
81 | .new-todo,
82 | .edit {
83 | position: relative;
84 | margin: 0;
85 | width: 100%;
86 | font-size: 24px;
87 | font-family: inherit;
88 | font-weight: inherit;
89 | line-height: 1.4em;
90 | color: inherit;
91 | padding: 6px;
92 | border: 1px solid #999;
93 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
94 | box-sizing: border-box;
95 | -webkit-font-smoothing: antialiased;
96 | -moz-osx-font-smoothing: grayscale;
97 | }
98 |
99 | .new-todo {
100 | padding: 16px 16px 16px 60px;
101 | height: 65px;
102 | border: none;
103 | background: rgba(0, 0, 0, 0.003);
104 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
105 | }
106 |
107 | .main {
108 | position: relative;
109 | z-index: 2;
110 | border-top: 1px solid #e6e6e6;
111 | }
112 |
113 | .toggle-all {
114 | width: 1px;
115 | height: 1px;
116 | border: none; /* Mobile Safari */
117 | opacity: 0;
118 | position: absolute;
119 | right: 100%;
120 | bottom: 100%;
121 | }
122 |
123 | .toggle-all + label {
124 | display: flex;
125 | align-items: center;
126 | justify-content: center;
127 | width: 45px;
128 | height: 65px;
129 | font-size: 0;
130 | position: absolute;
131 | top: -65px;
132 | left: -0;
133 | }
134 |
135 | .toggle-all + label:before {
136 | content: '❯';
137 | display: inline-block;
138 | font-size: 22px;
139 | color: #949494;
140 | padding: 10px 27px 10px 27px;
141 | -webkit-transform: rotate(90deg);
142 | transform: rotate(90deg);
143 | }
144 |
145 | .toggle-all:checked + label:before {
146 | color: #484848;
147 | }
148 |
149 | .todo-list {
150 | margin: 0;
151 | padding: 0;
152 | list-style: none;
153 | }
154 |
155 | .todo-list li {
156 | position: relative;
157 | font-size: 24px;
158 | border-bottom: 1px solid #ededed;
159 | }
160 |
161 | .todo-list li:last-child {
162 | border-bottom: none;
163 | }
164 |
165 | .todo-list li.editing {
166 | border-bottom: none;
167 | padding: 0;
168 | }
169 |
170 | .todo-list li.editing .edit {
171 | display: block;
172 | width: calc(100% - 43px);
173 | padding: 12px 16px;
174 | margin: 0 0 0 43px;
175 | }
176 |
177 | .todo-list li.editing .view {
178 | display: none;
179 | }
180 |
181 | .todo-list li .toggle {
182 | text-align: center;
183 | width: 40px;
184 | /* auto, since non-WebKit browsers doesn't support input styling */
185 | height: auto;
186 | position: absolute;
187 | top: 0;
188 | bottom: 0;
189 | margin: auto 0;
190 | border: none; /* Mobile Safari */
191 | -webkit-appearance: none;
192 | appearance: none;
193 | }
194 |
195 | .todo-list li .toggle {
196 | opacity: 0;
197 | }
198 |
199 | .todo-list li .toggle + label {
200 | /*
201 | Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
202 | IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
203 | */
204 | background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23949494%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
205 | background-repeat: no-repeat;
206 | background-position: center left;
207 | }
208 |
209 | .todo-list li .toggle:checked + label {
210 | background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%2359A193%22%20stroke-width%3D%223%22%2F%3E%3Cpath%20fill%3D%22%233EA390%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22%2F%3E%3C%2Fsvg%3E');
211 | }
212 |
213 | .todo-list li label {
214 | overflow-wrap: break-word;
215 | padding: 15px 15px 15px 60px;
216 | display: block;
217 | line-height: 1.2;
218 | transition: color 0.4s;
219 | font-weight: 400;
220 | color: #484848;
221 | }
222 |
223 | .todo-list li.completed label {
224 | color: #949494;
225 | text-decoration: line-through;
226 | }
227 |
228 | .todo-list li .destroy {
229 | display: none;
230 | position: absolute;
231 | top: 0;
232 | right: 10px;
233 | bottom: 0;
234 | width: 40px;
235 | height: 40px;
236 | margin: auto 0;
237 | font-size: 30px;
238 | color: #949494;
239 | transition: color 0.2s ease-out;
240 | }
241 |
242 | .todo-list li .destroy:hover,
243 | .todo-list li .destroy:focus {
244 | color: #C18585;
245 | }
246 |
247 | .todo-list li .destroy:after {
248 | content: '×';
249 | display: block;
250 | height: 100%;
251 | line-height: 1.1;
252 | }
253 |
254 | .todo-list li:hover .destroy {
255 | display: block;
256 | }
257 |
258 | .todo-list li .edit {
259 | display: none;
260 | }
261 |
262 | .todo-list li.editing:last-child {
263 | margin-bottom: -1px;
264 | }
265 |
266 | .footer {
267 | padding: 10px 15px;
268 | height: 20px;
269 | text-align: center;
270 | font-size: 15px;
271 | border-top: 1px solid #e6e6e6;
272 | }
273 |
274 | .footer:before {
275 | content: '';
276 | position: absolute;
277 | right: 0;
278 | bottom: 0;
279 | left: 0;
280 | height: 50px;
281 | overflow: hidden;
282 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
283 | 0 8px 0 -3px #f6f6f6,
284 | 0 9px 1px -3px rgba(0, 0, 0, 0.2),
285 | 0 16px 0 -6px #f6f6f6,
286 | 0 17px 2px -6px rgba(0, 0, 0, 0.2);
287 | }
288 |
289 | .todo-count {
290 | float: left;
291 | text-align: left;
292 | }
293 |
294 | .todo-count strong {
295 | font-weight: 300;
296 | }
297 |
298 | .filters {
299 | margin: 0;
300 | padding: 0;
301 | list-style: none;
302 | position: absolute;
303 | right: 0;
304 | left: 0;
305 | }
306 |
307 | .filters li {
308 | display: inline;
309 | }
310 |
311 | .filters li a {
312 | color: inherit;
313 | margin: 3px;
314 | padding: 3px 7px;
315 | text-decoration: none;
316 | border: 1px solid transparent;
317 | border-radius: 3px;
318 | }
319 |
320 | .filters li a:hover {
321 | border-color: #DB7676;
322 | }
323 |
324 | .filters li a.selected {
325 | border-color: #CE4646;
326 | }
327 |
328 | .clear-completed,
329 | html .clear-completed:active {
330 | float: right;
331 | position: relative;
332 | line-height: 19px;
333 | text-decoration: none;
334 | cursor: pointer;
335 | }
336 |
337 | .clear-completed:hover {
338 | text-decoration: underline;
339 | }
340 |
341 | .info {
342 | margin: 65px auto 0;
343 | color: #4d4d4d;
344 | font-size: 11px;
345 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
346 | text-align: center;
347 | }
348 |
349 | .info p {
350 | line-height: 1;
351 | }
352 |
353 | .info a {
354 | color: inherit;
355 | text-decoration: none;
356 | font-weight: 400;
357 | }
358 |
359 | .info a:hover {
360 | text-decoration: underline;
361 | }
362 |
363 | /*
364 | Hack to remove background from Mobile Safari.
365 | Can't use it globally since it destroys checkboxes in Firefox
366 | */
367 | @media screen and (-webkit-min-device-pixel-ratio:0) {
368 | .toggle-all,
369 | .todo-list li .toggle {
370 | background: none;
371 | }
372 |
373 | .todo-list li .toggle {
374 | height: 40px;
375 | }
376 | }
377 |
378 | @media (max-width: 430px) {
379 | .footer {
380 | height: 50px;
381 | }
382 |
383 | .filters {
384 | bottom: 10px;
385 | }
386 | }
387 |
388 | :focus,
389 | .toggle:focus + label,
390 | .toggle-all:focus + label {
391 | box-shadow: 0 0 2px 2px #CF7D7D;
392 | outline: 0;
393 | }
394 |
--------------------------------------------------------------------------------
/src/vrac/dsl/macro.cljc:
--------------------------------------------------------------------------------
1 | (ns ^:no-doc vrac.dsl.macro
2 | (:refer-clojure :exclude [destructure])
3 | (:require [mate.core :as mc]))
4 |
5 | (defn thread-first [[_ x & forms]]
6 | (loop [x x
7 | forms (seq forms)]
8 | (if (nil? forms)
9 | x
10 | (let [form (first forms)]
11 | (recur (if (seq? form)
12 | (-> `(~(first form) ~x ~@(next form))
13 | (with-meta (meta form)))
14 | `(~form ~x))
15 | (next forms))))))
16 |
17 | (defn thread-last [[_ x & forms]]
18 | (loop [x x
19 | forms (seq forms)]
20 | (if (nil? forms)
21 | x
22 | (let [form (first forms)]
23 | (recur (if (seq? form)
24 | (-> `(~@form ~x)
25 | (with-meta (meta form)))
26 | `(~form ~x))
27 | (next forms))))))
28 |
29 | (defn thread-as [[_ expr name & forms]]
30 | `(let [~name ~expr
31 | ~@(interleave (repeat name) (butlast forms))]
32 | ~(if (empty? forms)
33 | name
34 | (last forms))))
35 |
36 | (defn- destructure [bindings]
37 | (let [pairs (partition 2 bindings)]
38 | (if (every? (mc/comp-> first symbol?) pairs)
39 | bindings
40 | (let [destruct (fn destruct [[k-form v-form]]
41 | (cond
42 | ;; Sequential Destructuring
43 | (vector? k-form)
44 | (let [[k-form v-symb] (if (= (-> k-form pop peek) :as)
45 | [(-> k-form pop pop) (peek k-form)]
46 | [k-form (gensym "vec__")])
47 | [k-form &-symb] (if (= (-> k-form pop peek) '&)
48 | [(-> k-form pop pop) (peek k-form)]
49 | [k-form nil])]
50 | (-> (into [v-symb v-form]
51 | (mc/mapcat-indexed (fn [index k]
52 | (if (simple-symbol? k)
53 | [k `(nth ~v-symb ~index)]
54 | ;; map or vector
55 | (destruct [k `(nth ~v-symb ~index)]))))
56 | k-form)
57 | (cond->
58 | (some? &-symb)
59 | (conj &-symb `(drop ~(count k-form) ~v-symb)))))
60 |
61 | ;; Associative Destructuring
62 | (map? k-form)
63 | (let [v-symb (or (:as k-form) (gensym "map__"))
64 | or-hashmap (:or k-form)]
65 | (into [v-symb v-form]
66 | (mapcat (fn [[k v]]
67 | (cond
68 | (contains? #{:as :or} k)
69 | nil
70 |
71 | (= :& k)
72 | (let [kws (->> k-form
73 | (mapcat (fn [[k v]]
74 | (cond
75 | (simple-symbol? k)
76 | [v]
77 |
78 | (and (keyword? k)
79 | (= (name k) "keys"))
80 | (->> v
81 | (mapv (fn [s]
82 | (keyword (or (namespace s) (namespace k))
83 | (name s))))))))
84 | distinct)]
85 | [v `(dissoc ~v-symb ~@kws)])
86 |
87 | (and (keyword? k)
88 | (= (name k) "keys"))
89 | (let [k-ns (namespace k)
90 | symbols v]
91 | (into []
92 | (mapcat (fn [s]
93 | (let [s-name (name s)
94 | s-simple (symbol s-name)
95 | s-ns (namespace s)
96 | s-keyword (keyword (or s-ns k-ns) s-name)
97 | default (when (contains? or-hashmap s-simple)
98 | [(or-hashmap s-simple)])]
99 | [s-simple `(~s-keyword ~v-symb ~@default)])))
100 | symbols))
101 |
102 | (simple-symbol? k)
103 | (let [default (when (contains? or-hashmap k)
104 | [(or-hashmap k)])]
105 | [k `(~v ~v-symb ~@default)])
106 |
107 | :else ;; map or vector
108 | (destruct [k `(~v ~v-symb)]))))
109 | k-form))
110 |
111 | :else ;; No destructuring
112 | [k-form v-form]))]
113 | (into [] (mapcat destruct) pairs)))))
114 |
115 | (defn expand-let [original-form]
116 | (let [[_let bindings & bodies] original-form
117 | destructured-bindings (destructure bindings)
118 | has-multiple-bodies (> (count bodies) 1)
119 | body (if has-multiple-bodies
120 | `(~'do ~@bodies)
121 | (first bodies))]
122 | (if (and (identical? destructured-bindings bindings)
123 | (not has-multiple-bodies))
124 | original-form
125 | `(~'let ~destructured-bindings ~body))))
126 |
127 | (defn expand-for [original-form]
128 | (let [[_for bindings body] original-form
129 | new-bindings (->> (partition 2 bindings)
130 | (mapcat (fn [[k v]]
131 | (case k
132 | :let [:let (destructure v)]
133 | :when [:when v]
134 | :while [:while v]
135 | (if (simple-symbol? k)
136 | [k v]
137 | (let [item-symb (gensym "item__")]
138 | [item-symb v
139 | :let (destructure [k item-symb])])))))
140 | vec)]
141 | (if (and (= (count new-bindings) (count bindings))
142 | (->> (map vector new-bindings bindings)
143 | (every? (fn [[new-x x]] (identical? new-x x)))))
144 | original-form
145 | `(~'for ~new-bindings ~body))))
146 |
147 | (defn expand-when [original-form]
148 | (let [[_when condition & bodies] original-form
149 | has-multiple-bodies (> (count bodies) 1)
150 | body (if has-multiple-bodies
151 | `(~'do ~@bodies)
152 | (first bodies))]
153 | (if (not has-multiple-bodies)
154 | original-form
155 | `(~'when ~condition ~body))))
156 |
157 | (defn expand-fn [original-form]
158 | (let [[_fn fn-name params & bodies] (if (symbol? (second original-form)) ; is fn-name defined?
159 | original-form
160 | (list* 'fn nil (next original-form)))
161 | has-multiple-bodies (> (count bodies) 1)
162 | body (if has-multiple-bodies
163 | `(~'do ~@bodies)
164 | (first bodies))]
165 | ;; TODO: add support for the params destructuring
166 | (if (not has-multiple-bodies)
167 | original-form
168 | `(~'fn ~@(when (some? fn-name) [fn-name]) ~params ~body))))
169 |
170 | (defn expand-defn [original-form]
171 | (let [[_defn fn-name params & bodies] original-form
172 | has-multiple-bodies (> (count bodies) 1)
173 | body (if has-multiple-bodies
174 | `(~'do ~@bodies)
175 | (first bodies))]
176 | ;; TODO: add support for the params destructuring
177 | (if (not has-multiple-bodies)
178 | original-form
179 | `(~'defn ~fn-name ~params ~body))))
180 |
181 | (def default-macros
182 | {'-> thread-first
183 | '->> thread-last
184 | 'as-> thread-as
185 | 'let expand-let
186 | 'for expand-for
187 | 'when expand-when
188 | 'fn expand-fn
189 | 'defn expand-defn})
190 |
--------------------------------------------------------------------------------
/test/vrac/dsl/parser_test.cljc:
--------------------------------------------------------------------------------
1 | (ns vrac.dsl.parser-test
2 | (:require [clojure.test :refer [deftest testing is are]]
3 | [vrac.dsl :as dsl]
4 | [vrac.dsl.macro :as macro]
5 | [vrac.dsl.parser :as sut]))
6 |
7 | ;; If we want to test it on CLJS, we need to go through a macro
8 | ;; in order to get the env to be used in the `resolve` function.
9 | #?(:clj
10 | (deftest resolve-and-macro-expand-dsl-test
11 | (testing "expansion of the -> macro and symbol resolution in `let`"
12 | (is (= '(let [a 1]
13 | (clojure.core/inc a))
14 | (sut/resolve-and-macro-expand-dsl '(let [a 1]
15 | (-> a
16 | inc))
17 | (sut/symbol-resolver *ns* nil nil)
18 | macro/default-macros))))))
19 |
20 | (deftest expand-dsl-test
21 | (testing "expansion of the -> macro and symbol resolution in `let`"
22 | (is (= '(let [a 1]
23 | (clojure.core/inc a))
24 | (sut/expand-dsl
25 | (let [a 1]
26 | (-> a
27 | inc))))))
28 |
29 | (testing "destructuring in `for`"
30 | (is (= '(for [a [1 2 3]
31 | :let [b (clojure.core/+ a 100)
32 | m {:x a
33 | :y b}
34 | x (:x m)
35 | y (:y m)]
36 | :when (clojure.core/< a b x y)]
37 | [a b x y])
38 | (sut/expand-dsl
39 | (for [a [1 2 3]
40 | :let [b (+ a 100)
41 | {:keys [x y] :as m} {:x a
42 | :y b}]
43 | :when (< a b x y)]
44 | [a b x y])))))
45 |
46 | (testing "quoted expressions"
47 | (is (= '(let [a 1
48 | b 'x
49 | c ''y]
50 | 'd)
51 | (sut/expand-dsl
52 | (let [a 1
53 | b 'x
54 | c ''y]
55 | 'd)))))
56 |
57 | (testing "a signal"
58 | (is (= '(vrac.dsl/signal 1)
59 | (sut/expand-dsl
60 | (dsl/signal 1)))))
61 |
62 | (testing "a signal inside a signal"
63 | (is (= '(vrac.dsl/signal (vrac.dsl/signal 1))
64 | (sut/expand-dsl
65 | (dsl/signal (dsl/signal 1))))))
66 |
67 | (testing "a signal inside a +"
68 | (is (= '(clojure.core/+ (vrac.dsl/signal 1))
69 | (sut/expand-dsl
70 | (+ (dsl/signal 1))))))
71 |
72 | (testing "1 signal inside a let body"
73 | (is (= '(let []
74 | (vrac.dsl/signal 1))
75 | (sut/expand-dsl
76 | (let []
77 | (dsl/signal 1))))))
78 |
79 | (testing "1 signal inside a let binding"
80 | (is (= '(let [a (vrac.dsl/signal 1)]
81 | a)
82 | (sut/expand-dsl
83 | (let [a (dsl/signal 1)]
84 | a)))))
85 |
86 | (testing "effect"
87 | (is (= '(let [a 1
88 | b 2]
89 | (vrac.dsl/effect
90 | (clojure.core/prn (clojure.core/+ a b))))
91 | (sut/expand-dsl
92 | (let [a 1
93 | b 2]
94 | (dsl/effect
95 | (prn (+ a b))))))))
96 |
97 | (testing "effect-on"
98 | (is (= '(let [a 1
99 | b 2]
100 | (vrac.dsl/effect-on [a (clojure.core/even? b)]
101 | (clojure.core/prn (clojure.core/+ a b))))
102 | (sut/expand-dsl
103 | (let [a 1
104 | b 2]
105 | (dsl/effect-on [a (even? b)]
106 | (prn (+ a b))))))))
107 |
108 | (testing "defn"
109 | (let [expanded-dsl (sut/expand-dsl
110 | (defn foo [a ^bar b]
111 | a))]
112 | (is (= '(defn foo [a b]
113 | a)
114 | expanded-dsl))
115 | (testing "the metadata is preserved"
116 | (is (= {:tag 'bar}
117 | (-> expanded-dsl
118 | (nth 2)
119 | (nth 1)
120 | meta)))))))
121 |
122 | (deftest dsl->ast-test
123 | (are [expected-ast dsl]
124 | (= expected-ast (sut/dsl->ast dsl))
125 |
126 | {:node-type :clj/var
127 | :symbol 'a}
128 | 'a
129 |
130 | {:node-type :clj/value
131 | :value ''a}
132 | ''a
133 |
134 | {:node-type :clj/let
135 | :bindings [{:node-type :clj/let-binding
136 | :symbol 'a
137 | :value {:node-type :clj/value
138 | :value 1}}]
139 | :body {:node-type :clj/var
140 | :symbol 'a}}
141 | '(let [a 1]
142 | a)
143 |
144 | {:node-type :clj/let
145 | :bindings [{:node-type :clj/let-binding
146 | :symbol 'a
147 | :value {:node-type :clj/value
148 | :value 1}}
149 | {:node-type :clj/let-binding
150 | :symbol 'b
151 | :value {:node-type :clj/value
152 | :value ''x}}
153 | {:node-type :clj/let-binding
154 | :symbol 'c
155 | :value {:node-type :clj/value
156 | :value '''y}}]
157 | :body {:node-type :clj/value
158 | :value ''d}}
159 | '(let [a 1
160 | b 'x
161 | c ''y]
162 | 'd)
163 |
164 | {:node-type :dsl/signal
165 | :body {:node-type :clj/value
166 | :value 1}}
167 | '(vrac.dsl/signal 1)
168 |
169 | {:node-type :dsl/state
170 | :body {:node-type :clj/value
171 | :value 1}}
172 | '(vrac.dsl/state 1)
173 |
174 | {:node-type :clj/invocation
175 | :function {:node-type :clj/var
176 | :symbol 'clojure.core/+}
177 | :args [{:node-type :clj/var
178 | :symbol 'a}
179 | {:node-type :clj/var
180 | :symbol 'b}]}
181 | '(clojure.core/+ a b)
182 |
183 | {:node-type :dsl/once
184 | :body {:node-type :clj/invocation
185 | :function {:node-type :clj/var
186 | :symbol 'clojure.core/+}
187 | :args [{:node-type :clj/var
188 | :symbol 'a}
189 | {:node-type :clj/var
190 | :symbol 'b}]}}
191 | '(vrac.dsl/once (clojure.core/+ a b))
192 |
193 | {:node-type :dsl/memo
194 | :body {:node-type :clj/invocation
195 | :function {:node-type :clj/var
196 | :symbol 'clojure.core/+}
197 | :args [{:node-type :clj/var
198 | :symbol 'a}
199 | {:node-type :clj/var
200 | :symbol 'b}]}}
201 | '(vrac.dsl/memo (clojure.core/+ a b))
202 |
203 | {:node-type :clj/do
204 | :bodies [{:node-type :clj/invocation
205 | :function {:node-type :clj/var
206 | :symbol 'clojure.core/prn}
207 | :args [{:node-type :clj/var
208 | :symbol 'a}]}
209 | {:node-type :clj/var
210 | :symbol 'a}]}
211 | '(do (clojure.core/prn a)
212 | a)
213 |
214 | {:node-type :clj/if
215 | :cond {:node-type :clj/value
216 | :value true}
217 | :then {:node-type :clj/value
218 | :value 10}
219 | :else {:node-type :clj/value
220 | :value 20}}
221 | '(if true 10 20)
222 |
223 | {:node-type :clj/when
224 | :cond {:node-type :clj/value
225 | :value true}
226 | :body {:node-type :clj/value
227 | :value 30}}
228 | '(when true 30)
229 |
230 | {:node-type :clj/for
231 | :bindings [{:node-type :clj/for-iteration
232 | :symbol 'i
233 | :value {:node-type :clj/vector
234 | :items [{:node-type :clj/value
235 | :value 1}
236 | {:node-type :clj/value
237 | :value 2}
238 | {:node-type :clj/value
239 | :value 3}]}}
240 | {:node-type :clj/for-let
241 | :bindings [{:node-type :clj/let-binding
242 | :symbol 'j
243 | :value {:node-type :clj/value
244 | :value 4}}]}
245 | {:node-type :clj/for-when
246 | :cond {:node-type :clj/invocation
247 | :function {:node-type :clj/var
248 | :symbol 'clojure.core/>}
249 | :args [{:node-type :clj/var
250 | :symbol 'a}
251 | {:node-type :clj/value
252 | :value 1}]}}
253 | {:node-type :clj/for-while
254 | :cond {:node-type :clj/invocation
255 | :function {:node-type :clj/var
256 | :symbol 'clojure.core/>}
257 | :args [{:node-type :clj/var
258 | :symbol 'b}
259 | {:node-type :clj/value
260 | :value 2}]}}]
261 | :body {:node-type :clj/value
262 | :value 10}}
263 | '(for [i [1 2 3]
264 | :let [j 4]
265 | :when (clojure.core/> a 1)
266 | :while (clojure.core/> b 2)]
267 | 10)
268 |
269 | {:node-type :dsl/effect
270 | :body {:node-type :clj/invocation
271 | :function {:node-type :clj/var
272 | :symbol 'clojure.core/prn}
273 | :args [{:node-type :clj/invocation
274 | :function {:node-type :clj/var
275 | :symbol 'clojure.core/+}
276 | :args [{:node-type :clj/var
277 | :symbol 'a}
278 | {:node-type :clj/var
279 | :symbol 'b}]}]}}
280 | '(vrac.dsl/effect
281 | (clojure.core/prn (clojure.core/+ a b)))
282 |
283 | {:node-type :dsl/effect-on
284 | :triggers [{:node-type :clj/var
285 | :symbol 'a}
286 | {:node-type :clj/invocation
287 | :function {:node-type :clj/var
288 | :symbol 'clojure.core/even?}
289 | :args [{:node-type :clj/var
290 | :symbol 'b}]}]
291 | :body {:node-type :clj/invocation
292 | :function {:node-type :clj/var
293 | :symbol 'clojure.core/prn}
294 | :args [{:node-type :clj/invocation
295 | :function {:node-type :clj/var
296 | :symbol 'clojure.core/+}
297 | :args [{:node-type :clj/var
298 | :symbol 'a}
299 | {:node-type :clj/var
300 | :symbol 'b}]}]}}
301 | '(vrac.dsl/effect-on [a (clojure.core/even? b)]
302 | (clojure.core/prn (clojure.core/+ a b)))
303 |
304 | {:node-type :clj/defn
305 | :fn-name 'foo
306 | :params [{:node-type :clj/fn-param
307 | :symbol 'a}
308 | {:node-type :clj/fn-param
309 | :metadata {:tag 'bar}
310 | :symbol 'b}]
311 | :body {:node-type :clj/var
312 | :symbol 'a}}
313 |
314 | '(defn foo [a ^bar b]
315 | a)))
316 |
317 | (deftest ast->dsl-test
318 | (are [expected-dsl dsl]
319 | (= expected-dsl (-> dsl sut/dsl->ast sut/ast->dsl))
320 |
321 | '(fn [a b] a)
322 | '(fn [a b] a)
323 |
324 | '(fn foo [a b] a)
325 | '(fn foo [a b] a)
326 |
327 | '(defn foo [a b] a)
328 | '(defn foo [a b] a)
329 |
330 | '(let [a 1, b 2] a)
331 | '(let [a 1, b 2] a)
332 |
333 | '(do 1 2)
334 | '(do 1 2)
335 |
336 | '(if true 1 2)
337 | '(if true 1 2)
338 |
339 | '(when true 1)
340 | '(if true 1)
341 |
342 | '(when true 1)
343 | '(when true 1)
344 |
345 | '(for [x [1 2]
346 | :let [a 3, b 4]
347 | :when (clojure.core/even? a)
348 | :while (clojure.core/< a 10)]
349 | a)
350 | '(for [x [1 2]
351 | :let [a 3, b 4]
352 | :when (clojure.core/even? a)
353 | :while (clojure.core/< a 10)]
354 | a)
355 |
356 | '(clojure.core/+ 1 2)
357 | '(clojure.core/+ 1 2)
358 |
359 | '#{1 2}
360 | '#{1 2}
361 |
362 | '[1 2]
363 | '[1 2]
364 |
365 | '{:a 1, :b 2}
366 | '{:a 1, :b 2}
367 |
368 | `dsl/global
369 | `dsl/global
370 |
371 | `dsl/context
372 | `dsl/context
373 |
374 | ;;`(dsl/with-context {:a 1})
375 | ;;`(dsl/with-context {:a 1})
376 |
377 | `(dsl/once 1)
378 | `(dsl/once 1)
379 |
380 | `(dsl/signal 1)
381 | `(dsl/signal 1)
382 |
383 | `(dsl/state 1)
384 | `(dsl/state 1)
385 |
386 | `(dsl/memo (clojure.core/+ ~'a 1))
387 | `(dsl/memo (clojure.core/+ ~'a 1))
388 |
389 | `(dsl/effect (clojure.core/prn 1))
390 | `(dsl/effect (clojure.core/prn 1))
391 |
392 | `(dsl/effect-on ~'[a] 1)
393 | `(dsl/effect-on ~'[a] 1)))
394 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Eclipse Public License - v 2.0
2 |
3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE
4 | PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION
5 | OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
6 |
7 | 1. DEFINITIONS
8 |
9 | "Contribution" means:
10 |
11 | a) in the case of the initial Contributor, the initial content
12 | Distributed under this Agreement, and
13 |
14 | b) in the case of each subsequent Contributor:
15 | i) changes to the Program, and
16 | ii) additions to the Program;
17 | where such changes and/or additions to the Program originate from
18 | and are Distributed by that particular Contributor. A Contribution
19 | "originates" from a Contributor if it was added to the Program by
20 | such Contributor itself or anyone acting on such Contributor's behalf.
21 | Contributions do not include changes or additions to the Program that
22 | are not Modified Works.
23 |
24 | "Contributor" means any person or entity that Distributes the Program.
25 |
26 | "Licensed Patents" mean patent claims licensable by a Contributor which
27 | are necessarily infringed by the use or sale of its Contribution alone
28 | or when combined with the Program.
29 |
30 | "Program" means the Contributions Distributed in accordance with this
31 | Agreement.
32 |
33 | "Recipient" means anyone who receives the Program under this Agreement
34 | or any Secondary License (as applicable), including Contributors.
35 |
36 | "Derivative Works" shall mean any work, whether in Source Code or other
37 | form, that is based on (or derived from) the Program and for which the
38 | editorial revisions, annotations, elaborations, or other modifications
39 | represent, as a whole, an original work of authorship.
40 |
41 | "Modified Works" shall mean any work in Source Code or other form that
42 | results from an addition to, deletion from, or modification of the
43 | contents of the Program, including, for purposes of clarity any new file
44 | in Source Code form that contains any contents of the Program. Modified
45 | Works shall not include works that contain only declarations,
46 | interfaces, types, classes, structures, or files of the Program solely
47 | in each case in order to link to, bind by name, or subclass the Program
48 | or Modified Works thereof.
49 |
50 | "Distribute" means the acts of a) distributing or b) making available
51 | in any manner that enables the transfer of a copy.
52 |
53 | "Source Code" means the form of a Program preferred for making
54 | modifications, including but not limited to software source code,
55 | documentation source, and configuration files.
56 |
57 | "Secondary License" means either the GNU General Public License,
58 | Version 2.0, or any later versions of that license, including any
59 | exceptions or additional permissions as identified by the initial
60 | Contributor.
61 |
62 | 2. GRANT OF RIGHTS
63 |
64 | a) Subject to the terms of this Agreement, each Contributor hereby
65 | grants Recipient a non-exclusive, worldwide, royalty-free copyright
66 | license to reproduce, prepare Derivative Works of, publicly display,
67 | publicly perform, Distribute and sublicense the Contribution of such
68 | Contributor, if any, and such Derivative Works.
69 |
70 | b) Subject to the terms of this Agreement, each Contributor hereby
71 | grants Recipient a non-exclusive, worldwide, royalty-free patent
72 | license under Licensed Patents to make, use, sell, offer to sell,
73 | import and otherwise transfer the Contribution of such Contributor,
74 | if any, in Source Code or other form. This patent license shall
75 | apply to the combination of the Contribution and the Program if, at
76 | the time the Contribution is added by the Contributor, such addition
77 | of the Contribution causes such combination to be covered by the
78 | Licensed Patents. The patent license shall not apply to any other
79 | combinations which include the Contribution. No hardware per se is
80 | licensed hereunder.
81 |
82 | c) Recipient understands that although each Contributor grants the
83 | licenses to its Contributions set forth herein, no assurances are
84 | provided by any Contributor that the Program does not infringe the
85 | patent or other intellectual property rights of any other entity.
86 | Each Contributor disclaims any liability to Recipient for claims
87 | brought by any other entity based on infringement of intellectual
88 | property rights or otherwise. As a condition to exercising the
89 | rights and licenses granted hereunder, each Recipient hereby
90 | assumes sole responsibility to secure any other intellectual
91 | property rights needed, if any. For example, if a third party
92 | patent license is required to allow Recipient to Distribute the
93 | Program, it is Recipient's responsibility to acquire that license
94 | before distributing the Program.
95 |
96 | d) Each Contributor represents that to its knowledge it has
97 | sufficient copyright rights in its Contribution, if any, to grant
98 | the copyright license set forth in this Agreement.
99 |
100 | e) Notwithstanding the terms of any Secondary License, no
101 | Contributor makes additional grants to any Recipient (other than
102 | those set forth in this Agreement) as a result of such Recipient's
103 | receipt of the Program under the terms of a Secondary License
104 | (if permitted under the terms of Section 3).
105 |
106 | 3. REQUIREMENTS
107 |
108 | 3.1 If a Contributor Distributes the Program in any form, then:
109 |
110 | a) the Program must also be made available as Source Code, in
111 | accordance with section 3.2, and the Contributor must accompany
112 | the Program with a statement that the Source Code for the Program
113 | is available under this Agreement, and informs Recipients how to
114 | obtain it in a reasonable manner on or through a medium customarily
115 | used for software exchange; and
116 |
117 | b) the Contributor may Distribute the Program under a license
118 | different than this Agreement, provided that such license:
119 | i) effectively disclaims on behalf of all other Contributors all
120 | warranties and conditions, express and implied, including
121 | warranties or conditions of title and non-infringement, and
122 | implied warranties or conditions of merchantability and fitness
123 | for a particular purpose;
124 |
125 | ii) effectively excludes on behalf of all other Contributors all
126 | liability for damages, including direct, indirect, special,
127 | incidental and consequential damages, such as lost profits;
128 |
129 | iii) does not attempt to limit or alter the recipients' rights
130 | in the Source Code under section 3.2; and
131 |
132 | iv) requires any subsequent distribution of the Program by any
133 | party to be under a license that satisfies the requirements
134 | of this section 3.
135 |
136 | 3.2 When the Program is Distributed as Source Code:
137 |
138 | a) it must be made available under this Agreement, or if the
139 | Program (i) is combined with other material in a separate file or
140 | files made available under a Secondary License, and (ii) the initial
141 | Contributor attached to the Source Code the notice described in
142 | Exhibit A of this Agreement, then the Program may be made available
143 | under the terms of such Secondary Licenses, and
144 |
145 | b) a copy of this Agreement must be included with each copy of
146 | the Program.
147 |
148 | 3.3 Contributors may not remove or alter any copyright, patent,
149 | trademark, attribution notices, disclaimers of warranty, or limitations
150 | of liability ("notices") contained within the Program from any copy of
151 | the Program which they Distribute, provided that Contributors may add
152 | their own appropriate notices.
153 |
154 | 4. COMMERCIAL DISTRIBUTION
155 |
156 | Commercial distributors of software may accept certain responsibilities
157 | with respect to end users, business partners and the like. While this
158 | license is intended to facilitate the commercial use of the Program,
159 | the Contributor who includes the Program in a commercial product
160 | offering should do so in a manner which does not create potential
161 | liability for other Contributors. Therefore, if a Contributor includes
162 | the Program in a commercial product offering, such Contributor
163 | ("Commercial Contributor") hereby agrees to defend and indemnify every
164 | other Contributor ("Indemnified Contributor") against any losses,
165 | damages and costs (collectively "Losses") arising from claims, lawsuits
166 | and other legal actions brought by a third party against the Indemnified
167 | Contributor to the extent caused by the acts or omissions of such
168 | Commercial Contributor in connection with its distribution of the Program
169 | in a commercial product offering. The obligations in this section do not
170 | apply to any claims or Losses relating to any actual or alleged
171 | intellectual property infringement. In order to qualify, an Indemnified
172 | Contributor must: a) promptly notify the Commercial Contributor in
173 | writing of such claim, and b) allow the Commercial Contributor to control,
174 | and cooperate with the Commercial Contributor in, the defense and any
175 | related settlement negotiations. The Indemnified Contributor may
176 | participate in any such claim at its own expense.
177 |
178 | For example, a Contributor might include the Program in a commercial
179 | product offering, Product X. That Contributor is then a Commercial
180 | Contributor. If that Commercial Contributor then makes performance
181 | claims, or offers warranties related to Product X, those performance
182 | claims and warranties are such Commercial Contributor's responsibility
183 | alone. Under this section, the Commercial Contributor would have to
184 | defend claims against the other Contributors related to those performance
185 | claims and warranties, and if a court requires any other Contributor to
186 | pay any damages as a result, the Commercial Contributor must pay
187 | those damages.
188 |
189 | 5. NO WARRANTY
190 |
191 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT
192 | PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS"
193 | BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR
194 | IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF
195 | TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR
196 | PURPOSE. Each Recipient is solely responsible for determining the
197 | appropriateness of using and distributing the Program and assumes all
198 | risks associated with its exercise of rights under this Agreement,
199 | including but not limited to the risks and costs of program errors,
200 | compliance with applicable laws, damage to or loss of data, programs
201 | or equipment, and unavailability or interruption of operations.
202 |
203 | 6. DISCLAIMER OF LIABILITY
204 |
205 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT
206 | PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS
207 | SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
208 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST
209 | PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
210 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
211 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE
212 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE
213 | POSSIBILITY OF SUCH DAMAGES.
214 |
215 | 7. GENERAL
216 |
217 | If any provision of this Agreement is invalid or unenforceable under
218 | applicable law, it shall not affect the validity or enforceability of
219 | the remainder of the terms of this Agreement, and without further
220 | action by the parties hereto, such provision shall be reformed to the
221 | minimum extent necessary to make such provision valid and enforceable.
222 |
223 | If Recipient institutes patent litigation against any entity
224 | (including a cross-claim or counterclaim in a lawsuit) alleging that the
225 | Program itself (excluding combinations of the Program with other software
226 | or hardware) infringes such Recipient's patent(s), then such Recipient's
227 | rights granted under Section 2(b) shall terminate as of the date such
228 | litigation is filed.
229 |
230 | All Recipient's rights under this Agreement shall terminate if it
231 | fails to comply with any of the material terms or conditions of this
232 | Agreement and does not cure such failure in a reasonable period of
233 | time after becoming aware of such noncompliance. If all Recipient's
234 | rights under this Agreement terminate, Recipient agrees to cease use
235 | and distribution of the Program as soon as reasonably practicable.
236 | However, Recipient's obligations under this Agreement and any licenses
237 | granted by Recipient relating to the Program shall continue and survive.
238 |
239 | Everyone is permitted to copy and distribute copies of this Agreement,
240 | but in order to avoid inconsistency the Agreement is copyrighted and
241 | may only be modified in the following manner. The Agreement Steward
242 | reserves the right to publish new versions (including revisions) of
243 | this Agreement from time to time. No one other than the Agreement
244 | Steward has the right to modify this Agreement. The Eclipse Foundation
245 | is the initial Agreement Steward. The Eclipse Foundation may assign the
246 | responsibility to serve as the Agreement Steward to a suitable separate
247 | entity. Each new version of the Agreement will be given a distinguishing
248 | version number. The Program (including Contributions) may always be
249 | Distributed subject to the version of the Agreement under which it was
250 | received. In addition, after a new version of the Agreement is published,
251 | Contributor may elect to Distribute the Program (including its
252 | Contributions) under the new version.
253 |
254 | Except as expressly stated in Sections 2(a) and 2(b) above, Recipient
255 | receives no rights or licenses to the intellectual property of any
256 | Contributor under this Agreement, whether expressly, by implication,
257 | estoppel or otherwise. All rights in the Program not expressly granted
258 | under this Agreement are reserved. Nothing in this Agreement is intended
259 | to be enforceable by any entity that is not a Contributor or Recipient.
260 | No third-party beneficiary rights are created under this Agreement.
261 |
262 | Exhibit A - Form of Secondary Licenses Notice
263 |
264 | "This Source Code may also be made available under the following
265 | Secondary Licenses when the conditions for such availability set forth
266 | in the Eclipse Public License, v. 2.0 are satisfied: {name license(s),
267 | version(s), and exceptions or additional permissions here}."
268 |
269 | Simply including a copy of this Agreement, including this Exhibit A
270 | is not sufficient to license the Source Code under Secondary Licenses.
271 |
272 | If it is not possible or desirable to put the notice in a particular
273 | file, then You may include the notice in a location (such as a LICENSE
274 | file in a relevant directory) where a recipient would be likely to
275 | look for such a notice.
276 |
277 | You may add additional accurate notices of copyright ownership.
278 |
--------------------------------------------------------------------------------
/src/vrac/dsl/parser.cljc:
--------------------------------------------------------------------------------
1 | (ns ^:no-doc vrac.dsl.parser
2 | #?(:cljs (:require-macros [vrac.dsl.parser]))
3 | (:require [mate.core :as mc]
4 | [vrac.dsl :as dsl]
5 | [vrac.dsl.macro :as macro]))
6 |
7 | ;; Those are not functions or vars, they can't be resolved, so
8 | ;; we treat them as special cases which resolve to themselves.
9 | (def ^:private clj-reserved-symbols
10 | #{'do
11 | 'if
12 | 'when
13 | 'quote})
14 |
15 | (defn- rename-symb-ns [x rename-map]
16 | (if (symbol? x)
17 | (let [x-ns (namespace x)
18 | x-name (name x)
19 | x-name (rename-map x-name x-name)]
20 | (symbol x-ns x-name))
21 | x))
22 |
23 | (defn symbol-resolver [ns env symb-ns-rename-map]
24 | (fn [x]
25 | #?(:clj
26 | (when-some [resolved-x-var (ns-resolve ns env x)]
27 | (cond-> (symbol resolved-x-var)
28 | (some? symb-ns-rename-map) (rename-symb-ns symb-ns-rename-map))))))
29 |
30 | (defn resolve-and-macro-expand-dsl
31 | "Macro-expand and resolve the symbols in the DSL."
32 | [x resolve-symbol macros]
33 | (let [resolve-var (fn [x local-vars]
34 | (or (when (contains? clj-reserved-symbols x) x)
35 | (when (contains? macros x) x)
36 | (when (contains? local-vars x) x)
37 | (resolve-symbol x)
38 | (symbol (namespace x) (str (name x) "-not-found"))))
39 | resolve-and-expand (fn resolve-and-expand [x local-vars]
40 | (cond
41 | (seq? x)
42 | (if (zero? (count x))
43 | x
44 | (let [[f & args] x
45 | expanded-f (resolve-and-expand f local-vars)
46 | x (cons expanded-f args)
47 | macro-fn (get macros expanded-f)
48 | expanded-form (if (nil? macro-fn)
49 | x
50 | (macro-fn x))]
51 | (if-not (identical? x expanded-form)
52 | (recur expanded-form local-vars)
53 | (let [[f & args] x]
54 | ;;(prn [:expanded x])
55 | (cond
56 | ;; (defn ,,, [,,,] ,,,)
57 | (= f 'defn)
58 | (let [[fn-name params body] args
59 | local-vars (into local-vars params)
60 | body (resolve-and-expand body local-vars)]
61 | `(~'defn ~fn-name ~params ~body))
62 |
63 | ;; (let [,,,] ,,,)
64 | (= f 'let)
65 | (let [[bindings body] args
66 | [bindings local-vars] (reduce (fn [[bindings local-vars] [symbol-form value-form]]
67 | [(conj bindings symbol-form (resolve-and-expand value-form local-vars))
68 | (conj local-vars symbol-form)])
69 | [[] local-vars]
70 | (partition 2 bindings))
71 | body (resolve-and-expand body local-vars)]
72 | `(~'let ~bindings ~body))
73 |
74 | ;; (for [,,,] ,,,)
75 | (= f 'for)
76 | (let [[bindings body] args
77 | [bindings local-vars] (reduce (fn [[bindings local-vars] [symbol-form value-form]]
78 | (case symbol-form
79 | :let (let [[let-bindings local-vars] (reduce (fn [[bindings local-vars] [symbol-form value-form]]
80 | [(conj bindings symbol-form (resolve-and-expand value-form local-vars))
81 | (conj local-vars symbol-form)])
82 | [[] local-vars]
83 | (partition 2 value-form))]
84 | [(conj bindings :let let-bindings)
85 | local-vars])
86 | (:when :while) [(conj bindings symbol-form (resolve-and-expand value-form local-vars))
87 | local-vars]
88 | [(conj bindings symbol-form (resolve-and-expand value-form local-vars))
89 | (conj local-vars symbol-form)]))
90 | [[] local-vars]
91 | (partition 2 bindings))
92 | body (resolve-and-expand body local-vars)]
93 | `(~'for ~bindings ~body))
94 |
95 | ;; (quote ,,,)
96 | (= f 'quote)
97 | x ; "You can't touch this"
98 |
99 | ;; generic (f a b c)
100 | :else
101 | `(~expanded-f ~@(map (mc/partial-> resolve-and-expand local-vars) args)))))))
102 |
103 | (symbol? x)
104 | (resolve-var x local-vars)
105 |
106 | (vector? x)
107 | (mapv (mc/partial-> resolve-and-expand local-vars) x)
108 |
109 | (map? x)
110 | (-> x
111 | (update-keys (mc/partial-> resolve-and-expand local-vars))
112 | (update-vals (mc/partial-> resolve-and-expand local-vars)))
113 |
114 | (set? x)
115 | (set (map (mc/partial-> resolve-and-expand local-vars) x))
116 |
117 | :else
118 | x))]
119 | (resolve-and-expand x #{})))
120 |
121 | (defmacro expand-dsl [dsl-form]
122 | #?(:clj
123 | (let [ns *ns*
124 | env &env
125 | is-compiling-cljs-code (some? (:ns env))
126 | resolve-symbol (symbol-resolver ns env (when is-compiling-cljs-code {"cljs.core" "clojure.core"}))]
127 | `'~(resolve-and-macro-expand-dsl dsl-form
128 | resolve-symbol
129 | macro/default-macros))))
130 |
131 | (defn dsl->ast [x]
132 | "Shallow transformation from a DSL expression to an AST."
133 | (cond
134 |
135 | (seq? x)
136 | (if (zero? (count x))
137 | {:node-type :clj/value
138 | :value ()}
139 | (let [[f & args] x]
140 | (cond
141 | ;; (defn fn-name params body)
142 | (= f 'defn)
143 | (let [[fn-name params body] args]
144 | {:node-type :clj/defn
145 | :fn-name fn-name
146 | :params (->> params
147 | (mapv (fn [symbol]
148 | (let [metadata (meta symbol)]
149 | (-> {:node-type :clj/fn-param
150 | :symbol symbol}
151 | (cond-> (seq metadata) (assoc :metadata metadata)))))))
152 | :body (dsl->ast body)})
153 |
154 | ;; (let [,,,] ,,,)
155 | (= f 'let)
156 | (let [[bindings body] args]
157 | {:node-type :clj/let
158 | :bindings (->> bindings
159 | (partition 2)
160 | (mapv (fn [[symbol value]]
161 | {:node-type :clj/let-binding
162 | :symbol symbol
163 | :value (dsl->ast value)})))
164 | :body (dsl->ast body)})
165 |
166 | ;; (do ,,,)
167 | (= f 'do)
168 | (let [bodies args]
169 | {:node-type :clj/do
170 | :bodies (mapv dsl->ast bodies)})
171 |
172 | ;; (if cond then ?else)
173 | (= f 'if)
174 | (case (count args)
175 | 2 (let [[cond then] args]
176 | {:node-type :clj/when
177 | :cond (dsl->ast cond)
178 | :body (dsl->ast then)})
179 | 3 (let [[cond then else] args]
180 | {:node-type :clj/if
181 | :cond (dsl->ast cond)
182 | :then (dsl->ast then)
183 | :else (dsl->ast else)}))
184 |
185 | ;; (quote x)
186 | (= f 'quote)
187 | {:node-type :clj/value
188 | :value x}
189 |
190 | ;; (when cond body)
191 | (= f 'when)
192 | (let [[cond body] args]
193 | {:node-type :clj/when
194 | :cond (dsl->ast cond)
195 | :body (dsl->ast body)})
196 |
197 | ;; (with-context ,,,)
198 | (= f `dsl/with-context)
199 | (let [[context body] args]
200 | {:node-type :dsl/with-context
201 | :context (dsl->ast context)
202 | :body (dsl->ast body)})
203 |
204 | ;; (for [,,,] ,,,)
205 | (= f 'for)
206 | (let [[bindings body] args]
207 | {:node-type :clj/for
208 | :bindings (->> bindings
209 | (partition 2)
210 | (mapv (fn [[symbol value]]
211 | (case symbol
212 | :let {:node-type :clj/for-let
213 | :bindings (->> value
214 | (partition 2)
215 | (mapv (fn [[symbol value]]
216 | {:node-type :clj/let-binding
217 | :symbol symbol
218 | :value (dsl->ast value)})))}
219 | :when {:node-type :clj/for-when
220 | :cond (dsl->ast value)}
221 | :while {:node-type :clj/for-while
222 | :cond (dsl->ast value)}
223 | {:node-type :clj/for-iteration
224 | :symbol symbol
225 | :value (dsl->ast value)}))))
226 | :body (dsl->ast body)})
227 |
228 | ;; signal
229 | (= f `dsl/signal)
230 | (let [[body] args]
231 | {:node-type :dsl/signal
232 | :body (dsl->ast body)})
233 |
234 | ;; state
235 | (= f `dsl/state)
236 | (let [[body] args]
237 | {:node-type :dsl/state
238 | :body (dsl->ast body)})
239 |
240 | ;; memo
241 | (= f `dsl/memo)
242 | (let [[body] args]
243 | {:node-type :dsl/memo
244 | :body (dsl->ast body)})
245 |
246 | ;; once
247 | (= f `dsl/once)
248 | (let [[body] args]
249 | {:node-type :dsl/once
250 | :body (dsl->ast body)})
251 |
252 | ;; effect
253 | (= f `dsl/effect)
254 | (let [[body] args]
255 | {:node-type :dsl/effect
256 | :body (dsl->ast body)})
257 |
258 | ;; effect-on
259 | (= f `dsl/effect-on)
260 | (let [[triggers body] args]
261 | (assert (vector? triggers) "The triggers should be a vector literal.")
262 | {:node-type :dsl/effect-on
263 | :triggers (mapv dsl->ast triggers)
264 | :body (dsl->ast body)})
265 |
266 | :else
267 | {:node-type :clj/invocation
268 | :function (dsl->ast f)
269 | :args (mapv dsl->ast args)})))
270 |
271 | (set? x)
272 | {:node-type :clj/set
273 | :items (mapv dsl->ast x)}
274 |
275 | (vector? x)
276 | {:node-type :clj/vector
277 | :items (mapv dsl->ast x)}
278 |
279 | (map? x)
280 | {:node-type :clj/map
281 | :entries (->> x
282 | (mapv (fn [[k v]]
283 | {:node-type :clj/map-entry
284 | :key (dsl->ast k)
285 | :value (dsl->ast v)})))}
286 |
287 | (= x `dsl/global)
288 | {:node-type :dsl/global}
289 |
290 | (= x `dsl/context)
291 | {:node-type :dsl/context}
292 |
293 | (symbol? x)
294 | {:node-type :clj/var
295 | :symbol x}
296 |
297 | :else
298 | {:node-type :clj/value
299 | :value x}))
300 |
301 | ;; This is potentially useful for making the tests more humane.
302 | (defn ast->dsl [x]
303 | (case (:node-type x)
304 | :clj/fn
305 | `(~'fn ~@(when-some [fn-name (:fn-name x)] [fn-name])
306 | ~(mapv ast->dsl (:params x))
307 | ~(ast->dsl (:body x)))
308 |
309 | :clj/defn
310 | `(~'defn ~(:fn-name x)
311 | ~(mapv ast->dsl (:params x))
312 | ~(ast->dsl (:body x)))
313 |
314 | :clj/fn-param
315 | (:symbol x) ;; TODO: add the metadata
316 |
317 | :clj/let
318 | `(~'let ~(into [] (mapcat ast->dsl) (:bindings x))
319 | ~(ast->dsl (:body x)))
320 |
321 | :clj/let-binding
322 | [(:symbol x) (ast->dsl (:value x))]
323 |
324 | :clj/do
325 | `(~'do ~@(mapv ast->dsl (:bodies x)))
326 |
327 | :clj/if
328 | `(~'if ~(ast->dsl (:cond x))
329 | ~(ast->dsl (:then x))
330 | ~(ast->dsl (:else x)))
331 |
332 | :clj/when
333 | `(~'when ~(ast->dsl (:cond x))
334 | ~(ast->dsl (:body x)))
335 |
336 | :clj/for
337 | `(~'for ~(into [] (mapcat ast->dsl) (:bindings x))
338 | ~(ast->dsl (:body x)))
339 |
340 | :clj/for-iteration
341 | [(:symbol x) (ast->dsl (:value x))]
342 |
343 | :clj/for-let
344 | [:let (into [] (mapcat ast->dsl) (:bindings x))]
345 |
346 | :clj/for-when
347 | [:when (ast->dsl (:cond x))]
348 |
349 | :clj/for-while
350 | [:while (ast->dsl (:cond x))]
351 |
352 | :clj/invocation
353 | (list* (ast->dsl (:function x))
354 | (map ast->dsl (:args x)))
355 |
356 | :clj/var
357 | (:symbol x)
358 |
359 | :clj/value
360 | (:value x)
361 |
362 | :clj/set
363 | (into #{} (map ast->dsl) (:items x))
364 |
365 | :clj/vector
366 | (into [] (map ast->dsl) (:items x))
367 |
368 | :clj/map
369 | (into {} (map ast->dsl) (:entries x))
370 |
371 | :clj/map-entry
372 | [(ast->dsl (:key x)) (ast->dsl (:value x))]
373 |
374 | :dsl/global
375 | `dsl/global
376 |
377 | :dsl/context
378 | `dsl/context
379 |
380 | :dsl/with-context
381 | :not-implemented-yet
382 |
383 | :dsl/once
384 | `(dsl/once ~(ast->dsl (:body x)))
385 |
386 | :dsl/signal
387 | `(dsl/signal ~(ast->dsl (:body x)))
388 |
389 | :dsl/state
390 | `(dsl/state ~(ast->dsl (:body x)))
391 |
392 | :dsl/memo
393 | `(dsl/memo ~(ast->dsl (:body x)))
394 |
395 | :dsl/effect
396 | `(dsl/effect ~(ast->dsl (:body x)))
397 |
398 | :dsl/effect-on
399 | `(dsl/effect-on ~(mapv ast->dsl (:triggers x))
400 | ~(ast->dsl (:body x)))))
401 |
--------------------------------------------------------------------------------
/src/vrac/dsl/ast.cljc:
--------------------------------------------------------------------------------
1 | (ns ^:no-doc vrac.dsl.ast
2 | (:require #?(:clj [lambdaisland.deep-diff2 :refer [diff]])
3 | [mate.core :as mc]))
4 |
5 | ;; node-type -> child-node -> #{:one :many}
6 | (def node-type->walkable-children
7 | {:clj/fn [[:params :many]
8 | [:body :one]]
9 | :clj/defn [[:params :many]
10 | [:body :one]]
11 | :clj/fn-param {}
12 | :clj/let [[:bindings :many]
13 | [:body :one]]
14 | :clj/let-binding {:value :one}
15 | :clj/do {:bodies :many}
16 | :clj/if {:cond :one
17 | :then :one
18 | :else :one}
19 | :clj/when {:cond :one
20 | :body :one}
21 | :clj/for [[:bindings :many]
22 | [:body :one]]
23 | :clj/for-iteration {:value :one}
24 | :clj/for-let {:bindings :many}
25 | :clj/for-when {:cond :one}
26 | :clj/for-while {:cond :one}
27 | :clj/invocation {:function :one
28 | :args :many}
29 | :clj/var {}
30 | :clj/value {}
31 | :clj/set {:items :many}
32 | :clj/vector {:items :many}
33 | :clj/map {:entries :many}
34 | :clj/map-entry {:key :one
35 | :value :one}
36 | :dsl/global {}
37 | :dsl/context {}
38 | :dsl/with-context {:context :one
39 | :body :one}
40 | :dsl/once {:body :one}
41 | :dsl/signal {:body :one}
42 | :dsl/state {:body :one}
43 | :dsl/memo {:body :one}
44 | :dsl/effect {:body :one}
45 | :dsl/effect-on {:triggers :many
46 | :body :one}})
47 |
48 | (defn walk-ast
49 | "Walks and transforms a context containing the AST."
50 | [context pre-process post-process]
51 | (let [walk (fn walk [original-context]
52 | (let [path (:path original-context)
53 | {:keys [root-ast] :as context} (pre-process original-context)
54 | ast (get-in root-ast path)
55 | field->cardinality (-> ast :node-type node-type->walkable-children)
56 | context (reduce (fn [context [field cardinality]]
57 | (case cardinality
58 | :one (-> context
59 | (assoc :path (conj path field))
60 | walk)
61 | :many (reduce (fn [context index]
62 | (-> context
63 | (assoc :path (conj path field index))
64 | walk))
65 | context
66 | (-> ast (get field) count range))))
67 | context
68 | field->cardinality)]
69 | (-> context
70 | (assoc :path path)
71 | (assoc :original-context original-context)
72 | post-process
73 | (dissoc :original-context))))]
74 | (walk context)))
75 |
76 | (defn make-context
77 | "Returns an initial context from a given AST."
78 | [ast]
79 | {:root-ast ast
80 | :path []})
81 |
82 | (defn push-ancestor-path [context]
83 | (-> context
84 | (update :ancestor-paths conj (:path context))))
85 |
86 | (defn pop-ancestor-path [context]
87 | (-> context
88 | (update :ancestor-paths pop)))
89 |
90 | ;; -----------------------------------
91 |
92 | (defn- assoc-from [m-to m-from k]
93 | (-> m-to
94 | (assoc k (get m-from k))))
95 |
96 | (defn- assoc-existing-from [m-to m-from k]
97 | (cond-> m-to
98 | (contains? m-from k)
99 | (assoc k (get m-from k))))
100 |
101 | ;; -----------------------------------
102 |
103 | (defn- link-vars-pre-walk
104 | "On each var node, assoc `:var.definition/path` to point where its value is defined.
105 | Assoc :var/unbound true instead if the var is unbound."
106 | [{:keys [root-ast path symbol->definition-path] :as context}]
107 | (let [ast (get-in root-ast path)]
108 | (case (:node-type ast)
109 | :clj/var
110 | (let [symbol (:symbol ast)
111 | definition-path (symbol->definition-path symbol)]
112 | (-> context
113 | (assoc-in (cons :root-ast path)
114 | (-> ast
115 | (mc/if-> (nil? definition-path)
116 | (assoc :var/unbound true)
117 | (assoc :var.definition/path definition-path))))))
118 |
119 | ;; else
120 | context)))
121 |
122 | (defn- link-vars-post-walk
123 | "Updates a hashmap symbol->value-path as we walk the AST, to keep track of
124 | what vars are in the current scope and where they are defined."
125 | [{:keys [root-ast path original-context] :as context}]
126 | (let [ast (get-in root-ast path)]
127 | (case (:node-type ast)
128 | ;; Add a var to the hashmap when we exit a let-binding, as it becomes available
129 | ;; in the next let-binding entries and the let's body.
130 | (:clj/fn-param :clj/let-binding :clj/for-iteration)
131 | (let [symbol (:symbol ast)]
132 | (-> context
133 | ;; Curate symbol->definition-path's content
134 | (update :symbol->definition-path
135 | assoc symbol path)))
136 |
137 | ;; Restore the hashmap when we exit the body/ies of its parent let or for node.
138 | (:clj/let :clj/for)
139 | (-> context
140 | ;; Pop symbol->definition-path back to its original state
141 | (assoc-from original-context :symbol->definition-path))
142 |
143 | ;; else
144 | context)))
145 |
146 | (defn link-vars-to-their-definition-pass
147 | "An AST pass which links the vars to their definition via `:var.value/path`."
148 | [context]
149 | (-> context
150 | (assoc :symbol->definition-path {}) ;; pass setup
151 | (walk-ast link-vars-pre-walk link-vars-post-walk)
152 | (dissoc :symbol->definition-path))) ;; pass clean up
153 |
154 | ;; -----------------------------------
155 |
156 | (defn- find-bound-value-usages-pre-walk
157 | "Collects all the var usages from the whole AST into
158 | a hashmap `:var.value/path` -> `:var.usage/paths`."
159 | [{:keys [root-ast path] :as context}]
160 | (let [ast (get-in root-ast path)]
161 | (case (:node-type ast)
162 | :clj/var
163 | (let [definition-path (:var.definition/path ast)]
164 | (-> context
165 | (cond-> (some? definition-path)
166 | (update-in [:definition-path->usage-paths definition-path] (fnil conj []) path))))
167 |
168 | ;; else
169 | context)))
170 |
171 | (defn- find-bound-value-usages-clean-up
172 | "From the hashmap, write down in the AST the usages of each bound value."
173 | [{:keys [root-ast] :as context}]
174 | (let [definition-path->usage-paths (:definition-path->usage-paths context)]
175 | (-> context
176 | (assoc :root-ast (reduce (fn [root-ast [value-path usage-paths]]
177 | (-> root-ast
178 | (assoc-in (conj value-path :var.usage/paths) usage-paths)))
179 | root-ast
180 | definition-path->usage-paths))
181 | (dissoc :definition-path->usage-paths))))
182 |
183 | ;; This pass works after `link-vars-to-their-definition-pass`
184 | (defn add-var-usage-pass
185 | "An AST pass which annotates all the var usages bi-directionally,
186 | via `:var.value/path` and `:var.usage/paths`."
187 | [context]
188 | (-> context
189 | (walk-ast find-bound-value-usages-pre-walk identity)
190 | find-bound-value-usages-clean-up))
191 |
192 | ;; -----------------------------------
193 |
194 | (defn- lifespan-pre-walk
195 | [{:keys [root-ast path lifespan-path] :as context}]
196 | (let [ast (get-in root-ast path)
197 | ;; The lifespan defined on the node takes priority
198 | lifespan-path (:node.lifespan/path ast lifespan-path)
199 | ;; Saves the value in the node
200 | ast (-> ast
201 | (assoc :node.lifespan/path lifespan-path))
202 | ;;
203 | ast (case (:node-type ast)
204 | :clj/defn
205 | (-> ast
206 | ;; Sets a lifespan on all the body node
207 | (assoc-in [:body :node.lifespan/path] (conj path :body)))
208 |
209 | :clj/if
210 | (-> ast
211 | ;; Sets a lifespan on the :then and :else nodes
212 | (assoc-in [:then :node.lifespan/path] (conj path :then))
213 | (assoc-in [:else :node.lifespan/path] (conj path :else)))
214 |
215 | :clj/when
216 | (-> ast
217 | ;; Sets a lifespan on all the body node
218 | (assoc-in [:body :node.lifespan/path] (conj path :body)))
219 |
220 | :clj/for
221 | (let [[bindings lifespan-path] (reduce (fn [[bindings lifespan] [index binding]]
222 | ;; Sets the lifespan on each binding
223 | [(conj bindings (-> binding
224 | (assoc :node.lifespan/path lifespan)))
225 | (if (= (:node-type binding) :clj/for-iteration)
226 | (conj path :bindings index :symbol)
227 | lifespan)])
228 | [[] lifespan-path]
229 | (mc/seq-indexed (:bindings ast)))]
230 | (-> ast
231 | (assoc :bindings bindings)
232 | ;; Sets a lifespan on the :body node
233 | (assoc-in [:body :node.lifespan/path] lifespan-path)))
234 |
235 | (:dsl/effect :dsl/effect-on)
236 | (-> ast
237 | (assoc-in [:body :node.lifespan/path] (conj path :body))
238 | (assoc-in [:body :node.lifespan/type] :non-reactive))
239 |
240 | ;; else
241 | ast)]
242 | (-> context
243 | (assoc :lifespan-path lifespan-path)
244 | (assoc-in (cons :root-ast path) ast))))
245 |
246 | (defn- lifespan-post-walk
247 | [{:keys [original-context] :as context}]
248 | (-> context
249 | (assoc :lifespan-path (:lifespan-path original-context))))
250 |
251 | (defn add-lifespan-pass
252 | "An AST pass which annotates all the nodes with a :node.lifespan/path
253 | pointing to the root of the scope which has the same lifespan."
254 | [context]
255 | (-> context
256 | (assoc :lifespan-path []) ; pass setup
257 | (walk-ast lifespan-pre-walk lifespan-post-walk)
258 | (dissoc :lifespan-path))) ; pass clean up
259 |
260 | ;; -----------------------------------
261 |
262 | (defn add-reactivity-type-pre-walk [context]
263 | context)
264 |
265 | (defn- comp-reactivities [reactivity-set]
266 | (cond
267 | (contains? reactivity-set :signal) :signal
268 | (contains? reactivity-set :memo) :memo
269 | (contains? reactivity-set :value) :value
270 | :else nil))
271 |
272 | (defn add-reactivity-type-post-walk [{:keys [root-ast path] :as context}]
273 | (let [ast (get-in root-ast path)
274 | ast (case (:node-type ast)
275 | :dsl/signal
276 | (-> ast
277 | (assoc :reactivity/type :signal))
278 |
279 | :dsl/state
280 | (-> ast
281 | (assoc :reactivity/type :memo))
282 |
283 | :dsl/memo
284 | (let [body-reactivity (:reactivity/type (:body ast))
285 | ast-node-reactivity (if (contains? #{:signal :memo} body-reactivity)
286 | :memo
287 | :value)]
288 | (-> ast
289 | (assoc :reactivity/type ast-node-reactivity)))
290 |
291 | (:dsl/once :clj/value)
292 | (-> ast
293 | (assoc :reactivity/type :value))
294 |
295 | :clj/var
296 | (let [{:keys [var.definition/path var/unbound]} ast
297 | definition (get-in root-ast path)]
298 | (-> ast
299 | (cond-> (and (not unbound)
300 | (contains? definition :reactivity/type))
301 | (assoc :reactivity/type (:reactivity/type definition)))))
302 |
303 | :clj/invocation
304 | (let [ast-node-reactivity-type (comp-reactivities (into #{} (map :reactivity/type) (:args ast)))]
305 | (-> ast
306 | (cond-> (some? ast-node-reactivity-type)
307 | (assoc :reactivity/type ast-node-reactivity-type))))
308 |
309 | (:clj/set :clj/vector)
310 | (let [ast-node-reactivity-type (comp-reactivities (into #{} (map :reactivity/type) (:items ast)))]
311 | (-> ast
312 | (cond-> (some? ast-node-reactivity-type)
313 | (assoc :reactivity/type ast-node-reactivity-type))))
314 |
315 | :clj/map
316 | (let [ast-node-reactivity-type (comp-reactivities (into #{} (map :reactivity/type) (:entries ast)))]
317 | (-> ast
318 | (cond-> (some? ast-node-reactivity-type)
319 | (assoc :reactivity/type ast-node-reactivity-type))))
320 |
321 | :clj/map-entry
322 | (let [ast-node-reactivity-type (comp-reactivities (into #{} (map :reactivity/type) [(:key ast) (:value ast)]))]
323 | (-> ast
324 | (cond-> (some? ast-node-reactivity-type)
325 | (assoc :reactivity/type ast-node-reactivity-type))))
326 |
327 | :clj/let
328 | (let [ast-node-reactivity-type (-> ast :body :reactivity/type)]
329 | (-> ast
330 | (cond-> (some? ast-node-reactivity-type)
331 | (assoc :reactivity/type ast-node-reactivity-type))))
332 |
333 | :clj/let-binding
334 | (let [ast-node-reactivity-type (-> ast :value :reactivity/type)]
335 | (-> ast
336 | (cond-> (some? ast-node-reactivity-type)
337 | (assoc :reactivity/type ast-node-reactivity-type))))
338 |
339 | :clj/for
340 | (let [ast-node-reactivity-type (-> ast :body :reactivity/type)]
341 | (-> ast
342 | (cond-> (some? ast-node-reactivity-type)
343 | (assoc :reactivity/type ast-node-reactivity-type))))
344 |
345 |
346 | :clj/for-iteration
347 | (let [ast-node-reactivity-type (-> ast :value :reactivity/type)]
348 | (-> ast
349 | (cond-> (some? ast-node-reactivity-type)
350 | (assoc :reactivity/type ast-node-reactivity-type))))
351 |
352 | :clj/fn-param
353 | (let [ast-node-reactivity-type (-> ast :metadata :tag
354 | {'value :value
355 | 'signal :signal
356 | 'memo :memo})]
357 | (-> ast
358 | (cond-> (some? ast-node-reactivity-type)
359 | (assoc :reactivity/type ast-node-reactivity-type))))
360 |
361 | ;; else
362 | ast)]
363 | (-> context
364 | (assoc-in (cons :root-ast path) ast))))
365 |
366 | (defn add-reactivity-type-pass
367 | ""
368 | [context]
369 | (-> context
370 | (walk-ast add-reactivity-type-pre-walk add-reactivity-type-post-walk)))
371 |
372 | ;; -----------------------------------
373 |
374 | (defn- hoist-invocations-pre-walk [{:keys [root-ast path] :as context}]
375 | context)
376 |
377 | (defn- hoist-invocations-post-walk [{:keys [root-ast path] :as context}]
378 | (let [ast (get-in root-ast path)
379 | ast (case (:node-type ast)
380 | ast)]
381 | (-> context
382 | (assoc-in (cons :root-ast path) ast))))
383 |
384 |
385 | (defn hoist-invocations-pass
386 | [context]
387 | (-> context
388 | (walk-ast hoist-invocations-pre-walk hoist-invocations-post-walk)))
389 |
390 | ;; -----------------------------------
391 |
392 | ;; This is a template for creating a new pass.
393 | ;; Copy/paste, rename those functions, then implement
394 |
395 | #_
396 | (defn- xxx-pre-walk [context]
397 | context)
398 |
399 | #_
400 | (defn- xxx-post-walk [{:keys [root-ast path] :as context}]
401 | (let [ast (get-in root-ast path)
402 | ast (case (:node-type ast)
403 | #_#_
404 | :clj/value ast
405 |
406 | ;; else
407 | ast)]
408 | (-> context
409 | (assoc-in (cons :root-ast path) ast))))
410 |
411 | #_
412 | (defn xxx-pass
413 | [context]
414 | (-> context
415 | (walk-ast xxx-pre-walk xxx-post-walk)))
416 |
417 | ;; -----------------------------------
418 |
419 | #_(diff *2 *1)
420 |
--------------------------------------------------------------------------------
/src/vrac/web.cljc:
--------------------------------------------------------------------------------
1 | (ns vrac.web
2 | #?(:cljs (:require-macros [vrac.web :refer [with-context
3 | with-context-update
4 | if-fragment
5 | case-fragment
6 | cond-fragment]]))
7 | (:require [clojure.string :as str]
8 | #?(:cljs [goog.object :as gobj])
9 | [signaali.reactive :as sr]))
10 |
11 | ;; ----------------------------------------------
12 |
13 | (def xmlns-math-ml "http://www.w3.org/1998/Math/MathML")
14 | (def xmlns-html "http://www.w3.org/1999/xhtml")
15 | (def xmlns-svg "http://www.w3.org/2000/svg")
16 |
17 | #_(def xmlns-by-kw
18 | {:math xmlns-math-ml
19 | :html xmlns-html
20 | :svg xmlns-svg})
21 |
22 | (def ^:private ^:dynamic *xmlns-kw* :none)
23 |
24 | ;; ----------------------------------------------
25 |
26 | (def ^:private ^:dynamic *userland-context*)
27 |
28 | (defn get-context []
29 | *userland-context*)
30 |
31 | (defmacro with-context [new-context vcup]
32 | #?(:clj
33 | `(binding [*userland-context* ~new-context]
34 | (process-vcup ~vcup))))
35 |
36 | (defmacro with-context-update [context-fn vcup]
37 | #?(:clj
38 | `(let [parent-context# *userland-context*
39 | new-context# (sr/create-derived (fn [] (~context-fn parent-context#)))]
40 | (with-context new-context# ~vcup))))
41 |
42 | ;; ----------------------------------------------
43 |
44 | (defrecord VcupNode [node-type children])
45 | (defrecord ReactiveFragment [reactive-node])
46 | (defrecord PropEffect [reactive+-props])
47 | (defrecord ComponentResult [effects elements])
48 |
49 | ;; ----------------------------------------------
50 |
51 | #?(:cljs
52 | (defn- dom-node? [x]
53 | (instance? js/Node x)))
54 |
55 | (defn- vcup-fragment? [x]
56 | (and (instance? VcupNode x)
57 | (= (:node-type x) :<>)))
58 |
59 | #?(:cljs
60 | (defn- vcup-element? [x]
61 | (and (instance? VcupNode x)
62 | (not= (:node-type x) :<>)
63 | (or (simple-keyword? (:node-type x))
64 | (instance? js/Element (:node-type x))))))
65 |
66 | (defn- component-invocation? [x]
67 | (and (instance? VcupNode x)
68 | (fn? (:node-type x))))
69 |
70 | (defn- reactive-fragment? [x]
71 | (instance? ReactiveFragment x))
72 |
73 | (defn- prop-effect? [x]
74 | (instance? PropEffect x))
75 |
76 | (defn- component-result? [x]
77 | (instance? ComponentResult x))
78 |
79 | (defn- reactive-node? [x]
80 | (instance? signaali.reactive.ReactiveNode x))
81 |
82 | (defn- prop-map? [x]
83 | (and (map? x)
84 | (not (record? x))))
85 |
86 | (defn- props? [x]
87 | (or (prop-map? x)
88 | (prop-effect? x)))
89 |
90 | ;; ----------------------------------------------
91 |
92 | ;;(defn component-result [& sub-results]
93 | ;; (apply merge-with into sub-results))
94 |
95 | ;; ----------------------------------------------
96 |
97 | (defn- ensure-coll [x]
98 | (cond-> x (not (coll? x)) vector))
99 |
100 | (defn- parse-element-tag [s]
101 | (reduce (fn [acc part]
102 | (case (subs part 0 1)
103 | "." (update acc :classes conj (subs part 1))
104 | "#" (assoc acc :id (subs part 1))
105 | (assoc acc :tag-name part)))
106 | {:tag-name "div"
107 | :id nil
108 | :classes []}
109 | (re-seq #"[#.]?[^#.]+" s)))
110 |
111 | (defn- style->str [x]
112 | (cond
113 | (map? x) (->> x
114 | (map (fn [[k v]] (str (name k) ": " v)))
115 | (str/join "; ")
116 | (not-empty))
117 | :else x))
118 |
119 | (defn- class->str [x]
120 | (when (some? x)
121 | (->> x
122 | (ensure-coll)
123 | (flatten)
124 | (remove nil?)
125 | (map name)
126 | (str/join " ")
127 | (not-empty))))
128 |
129 | (defn- compose-prop-maps [base new]
130 | (let [style (into (or (:style base) {}) (:style new))
131 | class (into (or (:class base) []) (some-> (:class new) ensure-coll))]
132 | (-> base
133 | (into (dissoc new :style :class))
134 | (cond-> (seq style) (assoc :style style))
135 | (cond-> (seq class) (assoc :class class)))))
136 |
137 | ;; ----------------------------------------------
138 |
139 | (defn- deref+ [x]
140 | (cond
141 | (instance? signaali.reactive.ReactiveNode x) @x
142 | (fn? x) (x)
143 | :else x))
144 |
145 | #?(:cljs
146 | (defn- refs-effect [^js/Element element refs]
147 | (sr/create-effect
148 | (fn []
149 | (doseq [ref refs]
150 | (reset! ref element))
151 | (sr/on-clean-up (fn []
152 | (doseq [ref refs]
153 | (reset! ref nil))))))))
154 |
155 | #?(:cljs
156 | (defn- set-element-prop [xmlns-kw ^js/Element element prop-kw prop-value]
157 | (let [prop-ns (namespace prop-kw)
158 | prop-name (name prop-kw)]
159 | (cond
160 | (= prop-ns "a")
161 | (-> element (.setAttribute prop-name prop-value))
162 |
163 | (= prop-ns "p")
164 | (-> element (gobj/set prop-name prop-value))
165 |
166 | (= prop-ns "on")
167 | (-> element (.addEventListener prop-name prop-value))
168 |
169 | ;; TODO: see if we could use `classList` on the element
170 | (= prop-kw :class)
171 | (if (= xmlns-kw :none)
172 | (-> element (gobj/set "className" (class->str prop-value)))
173 | (-> element (.setAttribute "class" (class->str prop-value))))
174 |
175 | (= prop-kw :style)
176 | (if (= xmlns-kw :none)
177 | (-> element (gobj/set "style" (style->str prop-value)))
178 | (-> element (.setAttribute "style" (style->str prop-value))))
179 |
180 | (= prop-kw :ref)
181 | nil ;; no-op
182 |
183 | (str/starts-with? prop-name "data-")
184 | (-> element (.setAttribute prop-name prop-value))
185 |
186 | :else
187 | (let [prop-value (when-not (false? prop-value) prop-value)]
188 | (if (= xmlns-kw :none)
189 | (-> element (gobj/set prop-name prop-value))
190 | (-> element (.setAttribute prop-name prop-value))))))))
191 |
192 | #?(:cljs
193 | (defn- unset-element-prop [xmlns-kw ^js/Element element prop-kw prop-value]
194 | (let [prop-ns (namespace prop-kw)
195 | prop-name (name prop-kw)]
196 | (cond
197 | (= prop-ns "a")
198 | (-> element (.removeAttribute prop-name))
199 |
200 | (= prop-ns "p")
201 | (-> element (gobj/set prop-name nil))
202 |
203 | (= prop-ns "on")
204 | (-> element (.removeEventListener prop-name prop-value))
205 |
206 | (= prop-kw :class)
207 | (if (= xmlns-kw :none)
208 | (-> element (gobj/set "className" nil))
209 | (-> element (.removeAttribute "className")))
210 |
211 | (= prop-kw :style)
212 | (if (= xmlns-kw :none)
213 | (-> element (gobj/set "style" nil))
214 | (-> element (.removeAttribute "style")))
215 |
216 | (= prop-kw :ref)
217 | nil ;; no-op
218 |
219 | (str/starts-with? prop-name "data-")
220 | (-> element (.removeAttribute prop-name))
221 |
222 | :else
223 | (if (= xmlns-kw :none)
224 | (-> element (gobj/set prop-name nil))
225 | (-> element (.removeAttribute prop-name)))))))
226 |
227 | #?(:cljs
228 | (defn- dynamic-props-effect [xmlns-kw ^js/Element element attrs]
229 | (let [attrs (->> attrs
230 | ;; Combine the consecutive prop-maps together, and
231 | ;; unwrap the reactive+-props in PropEffect values.
232 | (into []
233 | (comp
234 | (partition-by prop-effect?)
235 | (mapcat (fn [prop-group]
236 | (if (prop-map? (first prop-group))
237 | [(reduce compose-prop-maps {} prop-group)]
238 | (mapv :reactive+-props prop-group)))))))
239 | old-props (atom nil)]
240 | (sr/create-effect (fn []
241 | (let [props (transduce (map deref+) compose-prop-maps {} attrs)]
242 | (doseq [[prop-kw prop-value] @old-props
243 | :when (not (contains? props prop-kw))]
244 | (unset-element-prop xmlns-kw element prop-kw prop-value))
245 |
246 | (doseq [[prop-kw prop-value] props]
247 | (set-element-prop xmlns-kw element prop-kw prop-value))
248 |
249 | (reset! old-props props)))))))
250 |
251 | #?(:cljs
252 | (defn- collect-dom-nodes! [^js/Array array x]
253 | (cond
254 | (instance? js/Node x)
255 | (.push array x)
256 |
257 | (vector? x)
258 | (doseq [item x]
259 | (collect-dom-nodes! array item))
260 |
261 | (reactive-fragment? x)
262 | (doseq [item @(:reactive-node x)]
263 | (collect-dom-nodes! array item)))))
264 |
265 |
266 | #?(:cljs
267 | (defn dynamic-children-effect
268 | "Dynamically update the DOM node so that its children keep representing the elements array.
269 | The elements are either js/Node (element or text node) or a reactive node whose value is
270 | a vector of js/Node instances."
271 | [^js/Element parent-element nodes]
272 | ;; TODO: This algorithm could be improved to only replace children where it is needed.
273 | ;; Would it be faster? less CPU-intensive?
274 | ;; maybe different algorithms depending on the size?
275 | ;; measurements needed.
276 | (let [xmlns-kw *xmlns-kw*
277 | userland-context *userland-context*]
278 | (sr/create-effect (fn []
279 | (binding [*xmlns-kw* xmlns-kw
280 | *userland-context* userland-context]
281 | (let [new-children (make-array 0)]
282 | (collect-dom-nodes! new-children nodes)
283 | (-> parent-element .-replaceChildren (.apply parent-element new-children)))))))))
284 |
285 | ;; ----------------------------------------------
286 |
287 | #?(:cljs
288 | (defn html-text-to-dom [html-text]
289 | (let [^js/Element element (js/document.createElement "div")]
290 | (set! (.-innerHTML element) html-text)
291 | (.-firstElementChild element))))
292 |
293 | (def ^:private inline-seq-children-xf
294 | (mapcat (fn [child] ;; Inline when child is a seq.
295 | (if (seq? child)
296 | child
297 | [child]))))
298 |
299 | (defn $ [node-type & children]
300 | (VcupNode. node-type children))
301 |
302 | ;; ----------------------------------------------
303 |
304 | #?(:cljs
305 | (defn process-vcup [vcup]
306 | (let [all-effects (atom [])
307 | to-dom-elements (fn to-dom-elements [vcup]
308 | (cond
309 | (nil? vcup)
310 | []
311 |
312 | (dom-node? vcup)
313 | [vcup]
314 |
315 | ;; Reactive fragment (i.e. if-fragment and for-fragment)
316 | (reactive-fragment? vcup)
317 | [vcup]
318 |
319 | (props? vcup)
320 | (throw (js/Error. "Props cannot be at the root of a scope."))
321 |
322 | ;; Component result (when the component is directly invoked)
323 | (component-result? vcup)
324 | (let [{:keys [effects elements]} vcup]
325 | (swap! all-effects into effects)
326 | elements)
327 |
328 | ;; Component invocation
329 | (component-invocation? vcup)
330 | (let [{component-fn :node-type
331 | args :children} vcup]
332 | (recur (apply component-fn args)))
333 |
334 | ;; Vcup fragment
335 | (vcup-fragment? vcup)
336 | (into []
337 | (comp inline-seq-children-xf
338 | (mapcat to-dom-elements))
339 | (:children vcup))
340 |
341 | ;; ($ :div ,,,)
342 | (vcup-element? vcup)
343 | (let [node-type (:node-type vcup)
344 | [xmlns-kw children-xmlns-kw ^js/Element element id classes] (if (instance? js/Element node-type)
345 | ;; DOM element
346 | (let [tag-name (.-tagName node-type)
347 | [xmlns-kw children-xmlns-kw] (case (str/lower-case tag-name)
348 | "svg" [:svg :svg]
349 | "math" [:math :math]
350 | "foreignobject" [*xmlns-kw* :none]
351 | [*xmlns-kw* *xmlns-kw*])]
352 | [xmlns-kw children-xmlns-kw node-type nil nil])
353 | ;; keywords like :div and :div#id.class1.class2
354 | (let [{:keys [tag-name id classes]} (parse-element-tag (name node-type))
355 | [xmlns-kw children-xmlns-kw] (case tag-name
356 | "svg" [:svg :svg]
357 | "math" [:math :math]
358 | "foreignObject" [*xmlns-kw* :none]
359 | [*xmlns-kw* *xmlns-kw*])
360 | element (case xmlns-kw
361 | :svg (js/document.createElementNS xmlns-svg tag-name)
362 | :math (js/document.createElementNS xmlns-math-ml tag-name)
363 | :none (js/document.createElement tag-name))]
364 | [xmlns-kw children-xmlns-kw element id classes]))
365 |
366 | children (:children vcup)
367 |
368 | ;; Collect all the props.
369 | props (cons (cond-> {}
370 | (some? id) (assoc :id id)
371 | (seq classes) (assoc :class classes))
372 | (filterv props? children))
373 |
374 | ;; Convert the children into elements.
375 | child-elements (binding [*xmlns-kw* children-xmlns-kw]
376 | (into []
377 | (comp (remove props?)
378 | inline-seq-children-xf
379 | (mapcat to-dom-elements))
380 | children))]
381 | ;; TODO: Can we use on-dispose instead?
382 | ;; Create an effect bound to the element's lifespan.
383 | ;; It is limited to statically declared :ref props.
384 | (let [refs (into []
385 | (comp (filter prop-map?)
386 | (keep :ref))
387 | props)]
388 | (when (seq refs)
389 | (swap! all-effects conj (refs-effect element refs))))
390 |
391 | ;; Set the element's props
392 | (if (every? prop-map? props)
393 | (let [composed-prop-maps (reduce compose-prop-maps {} props)]
394 | (doseq [[prop-kw prop-value] composed-prop-maps]
395 | (set-element-prop xmlns-kw element prop-kw prop-value)))
396 | (swap! all-effects conj (dynamic-props-effect xmlns-kw element props)))
397 |
398 | ;; Set the element's children
399 | (if (every? dom-node? child-elements)
400 | (doseq [child-element child-elements]
401 | (-> element (.appendChild child-element)))
402 | (swap! all-effects conj (dynamic-children-effect element child-elements)))
403 |
404 | ;; Result
405 | [element])
406 |
407 | (reactive-node? vcup)
408 | (let [^js/Text text-node (js/document.createTextNode "")
409 | effect (sr/create-effect (fn []
410 | (set! (.-nodeValue text-node) @vcup)))]
411 | (swap! all-effects conj effect)
412 | [text-node])
413 |
414 | :else
415 | [(js/document.createTextNode vcup)]))
416 |
417 | elements (to-dom-elements vcup)]
418 | (ComponentResult. @all-effects elements))))
419 |
420 | ;; ----------------------------------------------
421 |
422 | (defn use-effects [effects]
423 | (ComponentResult. effects nil))
424 |
425 | (defn props-effect [reactive+-props]
426 | (PropEffect. reactive+-props))
427 |
428 | ;; ----------------------------------------------
429 |
430 | ;; Q: Why was it an effect in the first place?
431 | ;; A: It is an effect because it owns effects, not computations.
432 | ;; The effect of the scope-effect is to trigger those effects when it is run,
433 | ;; which makes it effectful, so it needs to be an effect too.
434 | (defn scope-effect
435 | "This effects manages a static collection of effect's lifecycle so that they are
436 | first-run and disposed when this effect is run and cleaned up."
437 | ([owned-effects]
438 | (scope-effect owned-effects nil))
439 | ([owned-effects options]
440 | (when-some [owned-effects (seq (remove nil? owned-effects))]
441 | (let [scope (sr/create-effect (fn []
442 | (run! sr/run-if-needed owned-effects)
443 | (sr/on-clean-up (fn []
444 | (run! sr/dispose owned-effects))))
445 | options)]
446 | (doseq [owned-effect owned-effects]
447 | (sr/run-after owned-effect scope))
448 | scope))))
449 |
450 | (defn reactive-fragment
451 | ([vcup-fn]
452 | (reactive-fragment vcup-fn {:metadata {:name "reactive-fragment"}}))
453 | ([vcup-fn options]
454 | #?(:cljs
455 | (ReactiveFragment.
456 | (sr/create-derived (fn []
457 | (let [{:keys [effects elements]} (process-vcup (vcup-fn))]
458 | ;; scope-effect is inside the ReactiveFragment's effect because
459 | ;; we want its lifespan to be the period between 2 re-runs of this reactive node.
460 | (some-> (scope-effect effects {:dispose-on-zero-signal-watchers true})
461 | deref)
462 | elements))
463 | options)))))
464 |
465 | (defmacro when-fragment [reactive+-condition then-vcup-expr]
466 | #?(:clj
467 | `(let [reactive+-condition# ~reactive+-condition
468 | boolean-condition# (sr/create-memo (fn []
469 | (boolean (deref+ reactive+-condition#))))
470 | vcup-fn# (fn []
471 | (when @boolean-condition#
472 | ~then-vcup-expr))]
473 | (reactive-fragment vcup-fn# {:metadata {:name "when-fragment"}}))))
474 |
475 | (defmacro if-fragment [reactive+-condition then-vcup-expr else-vcup-expr]
476 | #?(:clj
477 | `(let [reactive+-condition# ~reactive+-condition
478 | boolean-condition# (sr/create-memo (fn []
479 | (boolean (deref+ reactive+-condition#))))
480 | vcup-fn# (fn []
481 | (if @boolean-condition#
482 | ~then-vcup-expr
483 | ~else-vcup-expr))]
484 | (reactive-fragment vcup-fn# {:metadata {:name "if-fragment"}}))))
485 |
486 | (defn- indexed-fragment [reactive-matched-index-or-nil
487 | clause-index->clause-vcup
488 | default-clause]
489 | #?(:cljs
490 | (reactive-fragment (fn []
491 | (let [matched-index @reactive-matched-index-or-nil]
492 | (cond
493 | (some? matched-index)
494 | (-> matched-index clause-index->clause-vcup)
495 |
496 | (= default-clause ::undefined)
497 | (throw (js/Error. "Missing default clause in indexed-fragment."))
498 |
499 | :else
500 | default-clause)))
501 | {:metadata {:name "index-fragment"}})))
502 |
503 | (defn case-fragment* [reactive+-value-expr
504 | clause-value->clause-index
505 | clause-index->clause-vcup
506 | default-clause]
507 | (let [reactive-matched-index-or-nil (sr/create-memo (fn [] (-> (deref+ reactive+-value-expr)
508 | clause-value->clause-index)))]
509 | (indexed-fragment reactive-matched-index-or-nil
510 | clause-index->clause-vcup
511 | default-clause)))
512 |
513 | (defmacro case-fragment [reactive+-value-expr & clauses]
514 | #?(:clj
515 | (let [[even-number-of-exprs default-clause] (if (even? (count clauses))
516 | [clauses ::undefined]
517 | [(butlast clauses) (last clauses)])
518 | clauses (partition 2 even-number-of-exprs)
519 | clause-value->clause-index (into {}
520 | (comp (map-indexed (fn [index [clause-value _clause-vcup]]
521 | (if (seq? clause-value)
522 | (->> clause-value
523 | (mapv (fn [clause-value-item]
524 | [clause-value-item index])))
525 | [[clause-value index]])))
526 | cat)
527 | clauses)
528 | clause-index->clause-vcup (mapv second clauses)]
529 | `(case-fragment* ~reactive+-value-expr
530 | ~clause-value->clause-index
531 | ~clause-index->clause-vcup
532 | ~default-clause))))
533 |
534 | (defn cond-fragment* [reactive-index-fn
535 | clause-index->clause-vcup]
536 | (indexed-fragment (sr/create-memo reactive-index-fn)
537 | clause-index->clause-vcup
538 | nil))
539 |
540 | (defmacro cond-fragment [& clauses]
541 | (assert (even? (count clauses)) "cond-fragment requires an even number of forms")
542 | #?(:clj
543 | (let [clauses (partition 2 clauses)
544 | clause-index->clause-vcup (mapv second clauses)]
545 | `(cond-fragment* (fn []
546 | (cond
547 | ~@(into []
548 | (comp (map-indexed (fn [index [clause-condition _clause-vcup]]
549 | [`~clause-condition index]))
550 | cat)
551 | clauses)))
552 | ~clause-index->clause-vcup))))
553 |
554 | #?(:cljs
555 | (defn for-fragment*
556 | ([reactive+-coll item-component]
557 | (for-fragment* reactive+-coll identity item-component))
558 | ([reactive+-coll key-fn item-component]
559 | (let [item-cache-atom (atom {})] ;; item-key -> [scope-effect elements]
560 | (ReactiveFragment.
561 | (sr/create-derived (fn []
562 | (let [coll (deref+ reactive+-coll)
563 |
564 | ;; Update the cache:
565 | ;; - only keep the current item keys,
566 | ;; - compute values for things not already cached.
567 | old-item-cache @item-cache-atom
568 | new-item-cache (into {}
569 | (map (fn [item]
570 | (let [item-key (key-fn item)]
571 | [item-key
572 | (if (contains? old-item-cache item-key)
573 | (get old-item-cache item-key)
574 | (let [{:keys [effects elements]} (process-vcup ($ item-component item))]
575 | [(scope-effect effects {:dispose-on-zero-signal-watchers true})
576 | elements]))])))
577 | coll)]
578 | (reset! item-cache-atom new-item-cache)
579 |
580 | ;; Return the aggregated elements
581 | (into []
582 | (mapcat (fn [item]
583 | (let [[scope elements] (get new-item-cache (key-fn item))]
584 | ;; Makes this signal depend on the scope.
585 | (some-> scope
586 | deref)
587 |
588 | ;; Return the elements
589 | elements)))
590 | coll)))
591 | {:metadata {:name "for-fragment"}}))))))
592 |
593 | ;; ----------------------------------------------
594 |
595 | #?(:cljs
596 | (defn- re-run-stale-effectful-nodes-at-next-frame []
597 | (js/requestAnimationFrame (fn []
598 | (sr/re-run-stale-effectful-nodes)
599 | (re-run-stale-effectful-nodes-at-next-frame)))))
600 |
601 | #?(:cljs
602 | (defn render [^js/Element parent-element vcup]
603 | ;; Remove all the children
604 | (.replaceChildren parent-element)
605 |
606 | (let [{:keys [effects]} (process-vcup ($ parent-element vcup))]
607 | ;; Run all the effects once, without using deref.
608 | (run! sr/run-if-needed effects)
609 |
610 | ;; Automatically refresh the DOM by re-running the effects which need a re-run.
611 | (re-run-stale-effectful-nodes-at-next-frame))))
612 |
613 | #?(:cljs
614 | (defn dispose-render-effects []
615 | ;; TODO: dispose all the effects used for the rendering and the DOM updates.
616 | ;; In this article, we will skip this step.
617 | ,))
618 |
--------------------------------------------------------------------------------
]