Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .clj-kondo/config.edn
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
;; A defui body is browser code, so it calls things that only exist there.
;; Reagami is loaded by index.html and never required on this side.
{:lint-as {buzz.core/defui clojure.core/defn
buzz.core/defn clojure.core/defn
buzz.core/defpart clojure.core/defn}
:linters {:unresolved-namespace {:exclude [reagami js]}}}
20 changes: 12 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,9 @@ values, call server actions, and keep local state:
- `(client expr)` passes a browser value to a `server!` action.
- `(local-state init)` creates a browser-local atom. Use `deref`, `reset!`,
and `swap!` to read and change it. Each mount keeps its own atom across
renders. The initial value can use a `server` expression.
renders. The initial value can use a `server` expression. It is also
computed for the first paint, so browser-only code in it needs `host`:
`(local-state (host :cljs (js/Date.)))` starts as nil on the first paint.

Use `reply` inside `server!` to return a value to the browser. Supply a Ring
response map as the second argument to set a cookie or other response headers:
Expand All @@ -94,18 +96,20 @@ response map as the second argument to set a cookie or other response headers:
(server! (reply :ok {:headers {"Set-Cookie" "session=abc; HttpOnly; Path=/"}}))
```

## Parts
## Functions

Use `defpart` to extract reusable UI functions from a component:
Use `buzz/defn` to define a function for the browser and the server:

```clojure
(defpart row [item]
(buzz/defn row [item]
[:li (:title item)])
```

Call `(row item)` inside `defui` or another part. Parts can call themselves
recursively and use `server!` for actions. Define `server` and `local-state`
in `defui`, then pass their values as arguments. See [doc/parts.md](doc/parts.md).
Call `(row item)` inside `defui` or another `buzz/defn`. These functions can
call themselves recursively and use `server!` for actions. Define `server` and
`local-state` in `defui`, then pass their values as arguments. Use `host`
where the browser and the server need different code. See
[doc/defn.md](doc/defn.md).

## Mounting

Expand Down Expand Up @@ -263,5 +267,5 @@ Omit `<!--app-->` to render that component only after the browser connects.

bb dev # the demo, plus an nrepl on 1667

Re-evaluate a `defui` or `defpart` to update open pages. Local state survives
Re-evaluate a `defui` or `buzz/defn` to update open pages. Local state survives
updates and reconnects when the number of `local-state` forms stays the same.
112 changes: 112 additions & 0 deletions doc/defn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Functions

Use `buzz/defn` to define a function for the browser and the server. Squint
compiles it for the browser. The first paint and server code call the function
compiled on the JVM. The function can return Hiccup or any other value, and
can call itself:

```clojure
(buzz/defn node [r]
[:li (:label r)
[:button {:on-click (fn [_] (server! (bump! (client (:id r)))))} "!"]
(when (seq (:children r))
[:ul (for [c (:children r)] (node c))])])

(defui viewer []
[:ul (node (server @tree))])
```

`defpart` is the former name of `buzz/defn`.

A plain `defn` is not compiled for the browser. Calling one from `defui` fails
in the browser with a ReferenceError.

## Arguments

A `buzz/defn` cannot contain `(server ...)` or `(local-state ...)`. Use these
forms in `defui` and pass their results as arguments:

```clojure
(buzz/defn row [item selected]
[:li {:class (when (= item @selected) "selected")
:on-click (fn [_] (reset! selected item))}
item])

(defui shelf [store]
(let [items (server @store)
selected (local-state nil)]
[:ul (for [item items]
(row item selected))]))
```

`selected` is browser state. The function receives the atom as an argument and
can read or update it.

## Handlers

A `buzz/defn` can contain `(server! ...)`. Wrap browser values in
`(client ...)` when sending them to the server.

Use `(buzz/request)` in a handler to access connection-scoped state:

```clojure
(defonce carts (atom {}))

(buzz/defn clear-button []
[:button
{:on-click (fn [_]
(server! (swap! carts assoc
(buzz/connection (buzz/request))
[])))}
"clear"])
```

## Browser and server branches

Use `host` where the browser and the server need different code. The browser
runs the `:cljs` branch. Server calls, including the first paint, run the
`:clj` branch. A missing branch uses `:default`, or nil:

```clojure
(buzz/defn parse-number [s]
(host :clj (Double/parseDouble s) :cljs (js/parseFloat s)))
```

A `:cljs` branch on its own makes a browser-only function:

```clojure
(buzz/defn commit! [pending* k]
(host :cljs (fn [raw]
(let [v (js/parseFloat raw)]
(when-not (js/isNaN v)
(server! (save! (client k) (client v))))
(swap! pending* dissoc k)))))
```

`host` is valid in `defui` and `buzz/defn` bodies, including event handlers.
It is refused inside `server` and `server!`. Outside `defui` and `buzz/defn`,
it uses the `:clj` branch, with the same fallback.

Use `host` for browser-only code in a `local-state` initial value. Initial
values are also computed for the first paint.

Inside a `fn`, `js/` interop throws if called during the first paint. Event
handlers do not run during the first paint and need no `host` for `js/`
interop. Outside a `fn`, wrap browser-only code in `host :cljs` to load the
definition.

Wrap calls to Squint-only functions, such as `clj->js`, in `host :cljs`,
including inside event handlers.

Read server state with `server` and run server actions with `server!`.
Use `host` to select code for each runtime.

## Editing in the REPL

Re-evaluate a `buzz/defn` in the REPL to hot-reload open pages.

## Limitations

- A `buzz/defn` takes one arity and a fixed number of arguments.
- A `buzz/defn` can call only functions that are already defined. Mutual
recursion requires re-evaluating the first definition after both exist.
79 changes: 0 additions & 79 deletions doc/parts.md

This file was deleted.

4 changes: 2 additions & 2 deletions examples/datalevin/src/buzz/dlv.clj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
(ns buzz.dlv
"A Datalevin browser over a MusicBrainz sample: a query editor with canned
queries, results as a table, and a query log shared by every viewer."
(:require [buzz.core :as buzz :refer [client defpart defui local-state observe reply server server!]]
(:require [buzz.core :as buzz :refer [client defui local-state observe reply server server!]]
[buzz.dlv.source :as dlv]
[clojure.edn :as edn]
[clojure.java.io :as io]
Expand Down Expand Up @@ -105,7 +105,7 @@
(sort-by :label)
vec)})

(defpart result-view [r]
(buzz/defn result-view [r]
(cond
(nil? r) [:p.hint "Run a query, or click one on the left."]
(:error r) [:div.error (:error r)]
Expand Down
6 changes: 3 additions & 3 deletions examples/tap-viewer/src/buzz/tap_viewer.clj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
(ns buzz.tap-viewer
"View `tap>` values in a browser."
(:require [buzz.core :as buzz :refer [client defpart defui local-state reply request server server!]]
(:require [buzz.core :as buzz :refer [client defui local-state reply request server server!]]
[clojure.java.io :as io]
[clojure.string :as str]
[org.httpkit.server :as http]))
Expand Down Expand Up @@ -186,7 +186,7 @@

;; A node renders its children by calling itself, so folding a branch drops the
;; whole subtree.
(defpart tree-node [n folded said]
(buzz/defn tree-node [n folded said]
[:div {:key (:path n)}
[:div.row
(if (:branch n)
Expand All @@ -210,7 +210,7 @@
(when (and (:branch n) (not (get @folded (:path n))))
[:div.kids (for [c (:children n)] (tree-node c folded said))])])

(defpart entry-item [e open folded said]
(buzz/defn entry-item [e open folded said]
[:li {:key (:id e)}
[:div.head
[:button.toggle {:on-click (fn [_] (swap! open (fn [m] (assoc m (:id e)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
;; Shipped with the library, so a project using it does not have to know that
;; defui and defpart bind their arguments.
;; defui, defn and defpart bind their arguments.
{:lint-as {buzz.core/defui clojure.core/defn
buzz.core/defn clojure.core/defn
buzz.core/defpart clojure.core/defn}}
4 changes: 2 additions & 2 deletions src/buzz/app.clj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
(ns buzz.app
(:require [babashka.nrepl.server :as nrepl]
[buzz.core :as buzz :refer [client defpart defui local-state observe reply
[buzz.core :as buzz :refer [client defui local-state observe reply
server server!]]
[clojure.string :as str]
[org.httpkit.server :as http]))
Expand Down Expand Up @@ -44,7 +44,7 @@
;; browser gets an `rpc!` call carrying `id` — which is a binding the browser
;; itself introduced, in the `for`.

(defpart todo-row [{:keys [id title done]}]
(buzz/defn todo-row [{:keys [id title done]}]
[:li {:key id}
[:input {:type "checkbox"
:checked done
Expand Down
4 changes: 2 additions & 2 deletions src/buzz/bench.clj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
over HTTP so the browser knows when it asked, and can time the whole loop:
server work, wire, and render."
(:require [babashka.nrepl.server :as nrepl]
[buzz.core :as buzz :refer [client defpart defui observe server server!]]
[buzz.core :as buzz :refer [client defui observe server server!]]
[clojure.string :as str]
[org.httpkit.server :as http]))

Expand Down Expand Up @@ -37,7 +37,7 @@

(defn clear! [] (reset! rows []))

(defpart row [{:keys [id label]}]
(buzz/defn row [{:keys [id label]}]
[:tr {:key id}
[:td.id id]
[:td.label label]
Expand Down
Loading