From ed6822988ab95dd58acb7488928aafa2c58e6296 Mon Sep 17 00:00:00 2001 From: zeitstein Date: Thu, 13 Jul 2023 11:45:09 -0600 Subject: [PATCH] docs: add docstrings for most commonly used APIs Identical content to https://github.com/thheller/shadow-grove/pull/2, just cleans up the commit log. All credit goes to @zeitstein --- src/main/shadow/grove.cljs | 214 +++++++++++++++++++++++++-- src/main/shadow/grove/db.cljc | 59 +++++++- src/main/shadow/grove/eql_query.cljc | 38 +++++ src/main/shadow/grove/history.cljs | 45 ++++++ src/main/shadow/grove/http_fx.cljs | 56 ++++++- 5 files changed, 392 insertions(+), 20 deletions(-) diff --git a/src/main/shadow/grove.cljs b/src/main/shadow/grove.cljs index f272140..52de89c 100644 --- a/src/main/shadow/grove.cljs +++ b/src/main/shadow/grove.cljs @@ -27,7 +27,10 @@ ;; used by shadow.grove.preload to funnel debug messages to devtools (def dev-log-handler nil) -(defn dispatch-up! [{::comp/keys [^not-native parent] :as env} ev-map] +((defn dispatch-up! + "Use within a component event handler to propagate the event `ev-map` up the + component tree. `env` is the environment map available in event handlers." + [{::comp/keys [^not-native parent] :as env} ev-map] {:pre [(map? env) (map? ev-map) (qualified-keyword? (:e ev-map))]} @@ -35,6 +38,24 @@ (gp/handle-event! parent ev-map nil env)) (defn query-ident + "Queries the db starting from `ident`. + * `query` - Optional. EQL query. Defaults to ident lookup if not provided. + * `config` - Optional. Any kind of config that may come up. + Changes to idents accessed by the query (including inside `eql/attr`) during + transactions will cause the query to re-un. + --- + Example + ```clojure + (defmethod eql/attr ::contains + [env db {:dir/keys [files dirs] :as current} query-part params] + (cond->> (concat dirs files) + (not (::show-hidden? db)) + (filterv (fn [ident] (not (::hidden? (get db ident))))))) + (bind {:as query-result + :dir/keys [name open?] + ::keys [contains]} + (sg/query-ident ident [:dir/name :dir/open? ::contains])) + ```" ;; shortcut for ident lookups that can skip EQL queries ([ident] {:pre [(db/ident? ident)]} @@ -51,6 +72,22 @@ (impl/slot-query ident query config))) (defn query-root + "Queries from the root of the db. + * `query` - EQL query. + * `config` - Optional. Any kind of config that may come up. + Changes to idents accessed by the query (including inside `eql/attr`) during + transactions will cause the query to re-un. + --- + Example + ```clojure + (defmethod eql/attr :products-in-stock [env db _ _] + (->> (db/all-of :product) + (filter #(pos? (:stock %))) + (mapv :db/ident))) + (defc ui-homepage [] + (bind {:keys [products-in-stock a-root-key]} + (sg/query-root [:products-in-stock :a-root-key])) + ```" ([query] (impl/slot-query nil query {})) ([query config] @@ -100,10 +137,23 @@ (apply update-fn state args))))) (defn run-tx + "Use inside a component event handler. Runs transaction `tx`, e.g. + `{:e ::some-event :data ...}`. `env` is the component environment map + available in event handlers. + --- + Example + ```clojure + (event :hide! [env ev-map event] + (when (.-ctrlKey event) + (sg/run-tx env ev-map))) + ```" [{::rt/keys [runtime-ref] :as env} tx] (impl/process-event runtime-ref tx env)) -(defn run-tx! [runtime-ref tx] +(defn run-tx! + "Runs the transaction `tx`, e.g. `{:e ::some-event :data ...}`, outside of the + component context." + [runtime-ref tx] (assert (rt/ref? runtime-ref) "expected runtime ref?") (let [{::rt/keys [scheduler]} @runtime-ref] (gp/run-now! scheduler #(impl/process-event runtime-ref tx nil) ::run-tx!))) @@ -115,9 +165,21 @@ (js-delete root-el "sg$env"))) (defn watch - "watches an atom and triggers an update on change + "Watches `watchable` and triggers an update on change accepts an optional path-or-fn arg that can be used for quick diffs + Accepts an optional `path-or-fn` arg that can be used to 'watch' a portion of + `the-atom`, enabling quick diffs. + * 'path' – as in `(get-in @the-atom path)` + * 'fn' - similar to above, defines how to access the relevant parts of + `the-atom`. Takes [old-state new-state] of `the-atom` and returns the actual + value stored in the hook. Example: `(fn [_ new] (get-in new [:id :name]))`. + + **Use strongly discouraged** in favor of the normalized central db. + + --- + Examples + ```clojure (watch the-atom [:foo]) (watch the-atom (fn [old new] ...))" ([watchable] @@ -132,6 +194,7 @@ (comp/atom-watch watchable path-or-fn)))) (defn env-watch + "Similar to [[watch]], but for atoms inside the component env." ([key-to-atom] (env-watch key-to-atom [] nil)) ([key-to-atom path] @@ -141,13 +204,46 @@ (vector? path)]} (comp/env-watch key-to-atom path default))) -(defn suspense [opts vnode] +(defn suspense + "See [docs](https://github.com/thheller/shadow-experiments/blob/master/doc/async.md)." + [opts vnode] (suspense/SuspenseInit. opts vnode)) -(defn simple-seq [coll render-fn] +(defn simple-seq + "Creates a collection of DOM elements by applying `render-fn` to each item + in `coll`. `render-fn` can be a function or component. + + Makes no attempts to minimize DOM operations required for updates. Efficient + with colls which change infrequently or colls updated at the tail. Otherwise, + consider using [[keyed-seq]]. + --- + Example: + ```clojure + (sg/simple-seq + (range 5) + (fn [num] + (<< [:div \"inline-item: \" num]))) + ```" + [coll render-fn] (sc/simple-seq coll render-fn)) -(defn keyed-seq [coll key-fn render-fn] +(defn keyed-seq + "Creates a keyed collection of DOM elements by applying `render-fn` to each + item in `coll`. + * `key-fn` is used to extract a unique key from items in `coll`. + * `render-fn` can be a function or component. + Uses the key to minimize DOM updates. Consider using instead of (the more + lightweight) [[simple-seq]] when `coll` changes frequently. + + --- + Examples: + ```clojure + ;; ident used as key + (keyed-seq [[::ident 1] ...] identity component) + (keyed-seq [{:id 1 :data ...} ...] :id + (fn [item] (<< [:div.id (:data item) ...]))) + ```" + [coll key-fn render-fn] (sc/keyed-seq coll key-fn render-fn)) (defn track-change @@ -173,20 +269,25 @@ (defn effect - "calls (callback env) after render when provided deps argument changes - callback can return a function which will be called if cleanup is required" + "Calls `(callback env)` after render when `deps` changes. (*Note*: will be + called on mount too.) `callback` may return a cleanup function which is + called on component unmount *and* just before whenever callback would be + called." [deps callback] {:pre [(fn? callback)]} (comp/slot-effect deps callback)) (defn render-effect - "call (callback env) after every render" + "Calls `(callback env)` after every render. `callback` may return a cleanup + function which is called on component unmount *and* after each render before + callback." [callback] {:pre [(fn? callback)]} (comp/slot-effect :render callback)) (defn mount-effect - "call (callback env) on mount once" + "Calls `(callback env)` on mount. `callback` may return a cleanup function + which is called on unmount." [callback] {:pre [(fn? callback)]} (comp/slot-effect :mount callback)) @@ -263,7 +364,12 @@ (set! (.-sg$env root-el) new-env) ::started))) -(defn render [rt-ref ^js root-el root-node] +(defn render + "Renders the UI root. Call on init and `^:dev/after-load`. + * `rt-ref` – runtime atom + * `root-el` – DOM element, e.g. `(js/document.getElementById \"app\")`. + * `root-node` – root fn/component (e.g. defined with `defc`)." + [rt-ref ^js root-el root-node] {:pre [(rt/ref? rt-ref)]} (gp/run-now! ^not-native (::rt/scheduler @rt-ref) #(render* rt-ref root-el root-node) ::render)) @@ -315,6 +421,19 @@ (set! update-pending? false))))) (defn prepare + "Initialises the runtime atom. + * `init` – Optional. A map. + * `data-ref` – Ref to the grove db atom. + * `app-id` + + --- + Example: + + ```clojure + (defonce rt-ref + (-> {::rt/tx-reporter (fn [report] (tap> report))} + (rt/prepare data-ref ::my-rt))) + ```" ([data-ref app-id] (prepare {} data-ref app-id)) ([init data-ref app-id] @@ -358,16 +477,81 @@ (defn vec-conj [x y] (if (nil? x) [y] (conj x y))) -(defn queue-fx [env fx-id fx-val] +(defn queue-fx + "Used inside an event handler, it queues up the registered handler of `fx-id` + to run at the end of the transaction. The handler will be called with + `fx-val`. + --- + Example: + ```clojure + (sg/reg-event rt-ref ::toggle-show-hidden! + (fn [tx-env {:keys [show?] :as event}] + (-> tx-env + (assoc-in [:db ::show-hidden?] show?) ;; modify the db + (sg/queue-fx ::alert! event)))) ;; schedule an fx + (sg/reg-fx rt-ref ::alert! + (fn [fx-env {:keys [show?] :as fx-val}] + (js/alert (str \"Will \" (when-not show? \"not\") \" show hidden files.\")))) + ```" + [env fx-id fx-val] (update env ::rt/fx vec-conj [fx-id fx-val])) -(defn reg-event [rt-ref ev-id handler-fn] +(defn reg-event + "Registers the `handler-fn` for event `ev-id`. `handler-fn` will be called + with `{:as tx-env :keys [db]} event-map` and should return the modified + `tx-env`. + + There is an alternative approach to registering event handlers, see examples. + + --- + Example: + ```clojure + (sg/reg-event rt-ref ::complete! + (fn [tx-env {:keys [checked ident] :as ev}] + (assoc-in tx-env [:db ident :completed?] checked))) + + ;; metadata approach + (defn complete! {::ev/handle ::complete!} + [tx-env {:keys [checked ident] :as ev}] + (assoc-in tx-env [:db ident :completed?] checked)) + + ;; use `{:dev/always true}` in namespaces utilising the metadata approach. + ```" + [rt-ref ev-id handler-fn] + {:pre [(keyword? ev-id) (ifn? handler-fn)]} (swap! rt-ref assoc-in [::rt/event-config ev-id] handler-fn) rt-ref) -(defn reg-fx [rt-ref fx-id handler-fn] +(defn reg-fx + "Registers the `handler-fn` for fx `fx-id`. fx is used for side effects, so + `handler-fn` shouldn't modify the db. + + --- + Examples: + ```clojure + (sg/reg-event rt-ref ::toggle-show-hidden! + (fn [tx-env {:keys [show?] :as event}] + (-> tx-env + (assoc-in [:db ::show-hidden?] show?) ;; modify the db + (sg/queue-fx ::alert! event)))) ;; schedule an fx + + (sg/reg-fx rt-ref ::alert! + (fn [fx-env {:keys [show?] :as fx-data}] + (js/alert (str \"Will \" (when-not show? \"not\") \" show hidden files.\")))) + ``` + + `(:transact! fx-env)` allows fx to schedule another transaction, but it + should be an async call: + ```clojure + (sg/reg-fx rt-ref :ui/redirect! + (fn [{:keys [transact!] :as env} {:keys [token title]}] + (let [tokens (str/split (subs token 1) #\"/\")] + ;; forcing the transaction to be async + (js/setTimeout #(transact! {:e :ui/route! :token token :tokens tokens}) 0)))) + ```" + [rt-ref fx-id handler-fn] (swap! rt-ref assoc-in [::rt/fx-config fx-id] handler-fn) rt-ref) @@ -410,4 +594,4 @@ ;; JS doesn't allow "prepping" an animation without a node reference ;; I prefer an API that lets me def an animation and then apply it to a node on demand (defn prepare-animation [keyframes options] - (->PreparedAnimation (clj->js keyframes) (clj->js options))) \ No newline at end of file + (->PreparedAnimation (clj->js keyframes) (clj->js options))) diff --git a/src/main/shadow/grove/db.cljc b/src/main/shadow/grove/db.cljc index ccfb185..17304b4 100644 --- a/src/main/shadow/grove/db.cljc +++ b/src/main/shadow/grove/db.cljc @@ -580,6 +580,24 @@ (if (instance? GroveDB db) @db db))) (defn configure + "Returns a [[GroveDB]] instance with associated `spec` as its schema used for + normalization operations. You may optionally initialize the db to an `init-db` + map. This will not normalize data from `init-db` in any way. + + --- + Example: + ``` + (def schema + {::folder + {:type :entity + :primary-key :id + :attrs {} + :joins {:folder/contains [:many ::file]}}}) + + (defonce data-ref + (-> (db/configure schema) + (atom))) + ```" ([spec] (configure {} spec)) ([init-db spec] @@ -681,6 +699,10 @@ imports)) (defn merge-seq + "Normalizes `coll` of items of `entity-type` into `data` (a [[configure]]d + [[GroveDB]] instance). The vector of idents normalized can be inserted at + target-path, replacing what's present. Alternatively, you can specify a target-fn + which will be called with `data normalized-idents` and should return updated `data`." ([data entity-type coll] (merge-seq data entity-type coll nil)) ([data entity-type coll target-path-or-fn] @@ -713,6 +735,26 @@ )))) (defn add + "Normalizes the `item` of `entity-type` into `data` at `target-path`. + * `data` - this should be a [[configure]]d [[GroveDB]] instance. + * `entity-type` of `item` being added. Should be present in db schema. + * `item` - a *map* of data to add. (For collections, see [[merge-seq]].) + * `target-path` - where to `conj` the ident of `item`. + --- + Examples: + ```clojure + ;; inside event handler + (update tx-env :db db/add ::node data-to-add [:root]) + ;; repl testing + (def schema + {::node + {:type :entity + :primary-key :id + :attrs {} + :joins {:children [:many ::node]}}}) + (-> (db/configure schema) + (db/add ::node {:id 0 :children [{:id 1} {:id 2}]} [::root])) + ```" ([data entity-type item] (add data entity-type item nil)) ([data entity-type item target-path] @@ -740,7 +782,10 @@ (vector? target-path) (update-in target-path conj ident)))))) -(defn update-entity [data entity-type id update-fn & args] +(defn update-entity + "Updates entity `(make-ident entity-type id)` in `data` (db) with + `(update-fn entity & args)`." + [data entity-type id update-fn & args] ;; FIXME: validate that both entity-type is defined and id matches type (update data (make-ident entity-type id) #(apply update-fn % args))) @@ -754,7 +799,12 @@ ;; keep this as the very last thing since we excluded clojure remove ;; don't want to write code that assumes it uses core remove -(defn remove [data thing] +(defn remove + "Removes `thing` from `data` root. `thing` can be either an ident or a map + like `{:db/ident ident ...}`. When used on a [[GroveDB]] instance, `thing` will + be removed from the set of all entities of `thing`'s type. Will *not* remove + any other references to `thing`." + [data thing] (cond (ident? thing) (dissoc data thing) @@ -765,6 +815,7 @@ :else (throw (ex-info "don't know how to remove thing" {:thing thing})))) -(defn remove-idents [data idents] +(defn remove-idents + "Given a coll of `idents`, [[remove]]s all corresponding entities from `data`." + [data idents] (reduce remove data idents)) - diff --git a/src/main/shadow/grove/eql_query.cljc b/src/main/shadow/grove/eql_query.cljc index b5b6d12..8264808 100644 --- a/src/main/shadow/grove/eql_query.cljc +++ b/src/main/shadow/grove/eql_query.cljc @@ -51,6 +51,44 @@ ;; but is the easiest to use with hot-reload in mind (defmulti attr + "Define 'computed attributes'. The attribute is the dispatch-fn of the method. + The return of this function will be included in query results for that + attribute. *Must not return lazy seq.* + The methods are called with the following args: + 1. `env` - component env. + 2. `db` - the db map. + 3. `current` - the entity which is the root of the query. For `query-ident` + this is the entity corresponding to the ident, for `query-root` it is the + whole db map. + 4. `query-part` – the dispatch-fn of the method, i.e. an EQL attribute. + 5. `params` - optional parameters specified with EQL attributes. + Called when building query results for each EQL attribute. See [[::default]]. + Idents accessed within the method will be 'observed': the query will re-run + if said idents are modified. + + --- + Example: + + ```clojure + ;; in a component + (bind query-result + (sg/query-ident ident [::foo '(::bar {:test 1})])) + ;; elsewhere + (defmethod attr ::foo + [env db current query-part params] + ;; the ::default impl + (get current query-part :db/undefined)) + ;; {:test 1} available in `params` for + (defmethod attr ::bar + [_ _ _ _ params]) + + ;; another example + (defmethod eql/attr ::m/num-active + [env db current _ params] + (->> (db/all-of db ::m/todo) + (remove ::m/completed?) + (count))) + ```" (fn [env db current query-part params] query-part) :default ::default) diff --git a/src/main/shadow/grove/history.cljs b/src/main/shadow/grove/history.cljs index f3de765..a9ec630 100644 --- a/src/main/shadow/grove/history.cljs +++ b/src/main/shadow/grove/history.cljs @@ -7,6 +7,51 @@ [shadow.arborist.attributes :as attr])) (defn init! + "Use to intercept clicks on 'internal' links and HTML `History` modifications to: + 1. update the state of `History` (e.g. update the URL), + 2. trigger the `:ui/route!` event (for which the handler is defined by the user). + `config` map: + * `:start-token` – replaces the \"/\" URL on init. Defaults to `\"/\"`. + Should start – but not end – with `/`. + * `:path-prefix` – a common URL base to use. Defaults to `\"\"`. + Should start – but not end – with `/`. + * `:use-fragment` – pass `true` if you want to use hash-based URLs. + Defaults to `false`. + * `:root-el` - optional DOM node to set the click listener on. + Defaults to `document.body`. + ### Usage + - This function should be called before component env is initialised (e.g. + before [[shadow.grove/render]]). *Note*: the `:ui/route!` event + will be triggered on call. + - Set the `:ui/href` attribute on anchors for internal links. (Unlike regular + `href`, will handle of `path-prefix` and `use-fragment`.) + - Register handler for the event `{:e :ui/route! :token token :tokens tokens}`. + - `token` is \"/a/b\" + - `tokens` is `[\"a\", \"b\"]`. + - Intercepts only pure clicks with main mouse button (no keyboard modifiers). + - The `:ui/redirect!` *fx* handler will be registered and can be 'called' with: + * `:token` – URL to redirect to (a string starting with `/`) + * `:title` – optional. `title` arg for `history.pushState` + + --- + Example: + ```clojure + ;; in a component + [:a {:ui/href (str \"/\" id)} \"internal link\"] + ;; handle clicks + (sg/reg-event rt-ref :ui/route! + (fn [env {:keys [token tokens]}] + (let [ident (db/make-ident ::entity (first tokens))] + (-> env + (assoc-in [:db ::main-root] ident) + (sg/queue-fx :ui/set-window-title! {:title (get-title ident)}))))) + ;; calling init + (defn ^:dev/after-load start [] + (sg/render rt-ref root-el (ui-root))) + (defn init [] + (history/init! rt-ref {}) + (start)) + ```" [rt-ref {:keys [start-token path-prefix use-fragment root-el] :or {start-token "/" diff --git a/src/main/shadow/grove/http_fx.cljs b/src/main/shadow/grove/http_fx.cljs index c54ae10..40d2809 100644 --- a/src/main/shadow/grove/http_fx.cljs +++ b/src/main/shadow/grove/http_fx.cljs @@ -256,7 +256,61 @@ (defn merge-right [left right] (merge right left)) -(defn make-handler [config] +(defn make-handler + "Returns an fx handler which will initiate an XMLHttpRequest. The request itself + is defined in the event handler queuing the fx, i.e. as `fx-val` arg of + [[shadow.grove/queue-fx]]. + ### Request definition + The request definition is a map with the following keys: + * `:request` – possible formats: + - `[method uri body opts]` + - `{:method , :uri , :body , :request-format ,}`. `:method` defaults to `POST` + if `:body` is present, `GET` otherwise. `:uri` defaults to empty string. + - `uri` string, same as `[:GET uri nil {}]`. + `uri` example: + ```clojure + [\"foo\" \"bar\" {:age 42 :name \"bob\"}] ;; \"foo/bar?age=42&name=bob\" + ``` + Keys allowed in `opts` (or in the map): `:request-format`, `:timeout` (see below). + * `:on-success` - event (map) to trigger if the request is successful + (status code < 400). The transformed (per `:response-formats`, see below) + response will be available in the event map under `:result`. + * `:request-many` – optional coll of requests. Only allowed instead of `:request`. + * `:on-error` – optional. See below. + ### `config` + `config` is a map with the following options: + * `:request-format` – sets the `content-type` of request header and transforms + request body. Value can be: + - `:edn` + - `:transit` (the `transit-str` needs to be available, see `grove.transit` ns.) + - a function `(request-format env body opts)` returning `[request-header request-body]`. + + Can be specified in: + 1. `opts` of the request definition + 2. `config` of `make-handler` + 3. `env` under `::http-fx/request-format` – the `fx-env` of fx where `make-handler` is used + * `:response-formats` - a map associating `content-type` of the response to + transformation functions handling the body of the response. The defaults can + be found in [[default-response-formats]]. Users can specify their own in + `config` of [[make-handler]]. + * `:on-error` – event (map) to trigger if the request returns an error + (status code >= 400). The response, status and the request itself will be + available in the event map under `:result`, `:status` and `:sent-request`. + Can be specified in: + 1. request definition + 2. `config` of `make-handler` + 3. `env` under `::http-fx/on-error` – the `fx-env` where `make-handler` is used. + * `:base-url` – part of the url common to all requests initiated by the handler. + * `:timeout` - sets `XMLHttpRequest.timeout`. Specified in `:opts` of the request. + * `:with-credentials` - boolean for `XMLHttpRequest.withCredentials`. Can be + specified in `:opts` of the request or `config` of [[make-handler]]. + + If using `transit`, add the following to your `init` fn: + ```clojure + (shadow.grove.transit/init! rt-ref) + ``` + There's also `shadow.grove.edn`." + [config] ;; FIXME: deep-merge, so maps are merged properly (let [config (update config :response-formats merge-right default-response-formats)] (fn http-fx-handler [env request-def]