Boundary marking and fragment extraction.
Resolves three problems that declaring boundaries separately created, all with
one mechanism: :render runs once and marks its own boundaries, and the engine
extracts fragments from the resulting tree.
:render-at duplicated :render. The slice had to maintain two copies of
the same markup — including id construction — which had to agree or patches
silently targeted nothing. Gone: there is one render function.:boundaries had to be declared separately from the render that implements
them, so the two could drift. Gone: boundaries are discovered from the
render output.component-id + path) while :render
emitted whatever the author wrote. They disagreed, and every patch targeted
a non-existent element with no error. Gone: the id is read from the
rendered element, so the author picks it and the engine cannot disagree.Usage — wrap each patchable region in boundary:
(defn render [{:keys [count items] :as view}]
(boundary []
[:div {:id "root"}
(boundary [:count] [:strong {:id "count"} count])
[:ul (for [i items]
(boundary [:items (:id i)]
[:li {:id (str "row-" (:id i))} (:text i)]))]]))
The path given to boundary is a view path — the same vocabulary the diff
engine emits — which is what lets a changed path resolve to the element that
owns it. Keyed collection items use their key, not their index, matching the
diff engine's addressing.
Marking is done with metadata, for the same reason keyed collections are
(remuda.diff): metadata does not participate in =, so it cannot
perturb value comparison.
Nothing here is HTML-specific — the examples use hiccup because that is what the
datastar binding renders, but the tag vocabulary is irrelevant and [:panel {...} [:dial {...} 7]] extracts exactly as well as a <div>. Two structural
requirements are real, though, and neither is checked:
extract descends through vectors and seqs only. A tree whose children
live under map values — {:kind :window :kids [...]} — yields only the root
boundary and silently loses every nested one. There is no error; patches for
those paths simply never resolve.(:id (second node)). So a marked node should be a
vector whose second element is an attribute map. Without one, :id is nil and
the binding falls back to deriving a target from the path, which is the
drift problem point 3 above exists to remove.Both are satisfied by anything hiccup-shaped, which is why they have never bitten. They are written down because "remuda does not know about HTML" is true and "remuda accepts any tree" is not.
Boundary marking and fragment extraction.
Resolves three problems that declaring boundaries separately created, all with
one mechanism: **`:render` runs once and marks its own boundaries, and the engine
extracts fragments from the resulting tree.**
1. `:render-at` duplicated `:render`. The slice had to maintain two copies of
the same markup — including id construction — which had to agree or patches
silently targeted nothing. Gone: there is one render function.
2. `:boundaries` had to be declared separately from the render that implements
them, so the two could drift. Gone: boundaries are discovered from the
render output.
3. Ids were *derived* by the engine (`component-id + path`) while `:render`
emitted whatever the author wrote. They disagreed, and every patch targeted
a non-existent element with no error. Gone: the id is read from the
rendered element, so the author picks it and the engine cannot disagree.
Usage — wrap each patchable region in `boundary`:
(defn render [{:keys [count items] :as view}]
(boundary []
[:div {:id "root"}
(boundary [:count] [:strong {:id "count"} count])
[:ul (for [i items]
(boundary [:items (:id i)]
[:li {:id (str "row-" (:id i))} (:text i)]))]]))
The path given to `boundary` is a *view* path — the same vocabulary the diff
engine emits — which is what lets a changed path resolve to the element that
owns it. Keyed collection items use their key, not their index, matching the
diff engine's addressing.
Marking is done with metadata, for the same reason keyed collections are
(`remuda.diff`): metadata does not participate in `=`, so it cannot
perturb value comparison.
## What the tree has to look like
Nothing here is HTML-specific — the examples use hiccup because that is what the
datastar binding renders, but the tag vocabulary is irrelevant and `[:panel {...}
[:dial {...} 7]]` extracts exactly as well as a `<div>`. Two structural
requirements are real, though, and neither is checked:
1. **`extract` descends through vectors and seqs only.** A tree whose children
live under map *values* — `{:kind :window :kids [...]}` — yields only the root
boundary and silently loses every nested one. There is no error; patches for
those paths simply never resolve.
2. **A node's id is read as `(:id (second node))`.** So a marked node should be a
vector whose second element is an attribute map. Without one, `:id` is nil and
the binding falls back to deriving a target from the path, which is the
drift problem point 3 above exists to remove.
Both are satisfied by anything hiccup-shaped, which is why they have never bitten.
They are written down because "remuda does not know about HTML" is true and
"remuda accepts any tree" is not.(boundary path node)Marks node as the patchable region owning view path path.
Returns node unchanged apart from metadata, so a marked element composes
anywhere an unmarked one would.
Marks `node` as the patchable region owning view path `path`. Returns `node` unchanged apart from metadata, so a marked element composes anywhere an unmarked one would.
(extract tree)Walks a rendered tree, returning {path {:node ... :id ...}} for every
marked boundary.
Nested boundaries are all collected: an item inside a marked list is found along with the list and the root, so a patch can target whichever granularity the diff asked for.
Descends through vectors and seqs only — see the namespace docstring. A tree nesting its children under map values loses every boundary below the root, with no error.
Walks a rendered `tree`, returning `{path {:node ... :id ...}}` for every
marked boundary.
Nested boundaries are all collected: an item inside a marked list is found
along with the list and the root, so a patch can target whichever granularity
the diff asked for.
Descends through **vectors and seqs only** — see the namespace docstring. A tree
nesting its children under map values loses every boundary below the root, with
no error.(resolve-boundary boundary-paths path)Nearest ancestor of path present in boundary-paths.
View paths are finer grained than DOM elements — [:items 2 :text] addresses a
text node — so a changed path must resolve up to the element that owns it.
Falls back to [], the component root.
:* in a declared path still matches any key, which keeps hand-declared
boundary sets working; extracted boundaries name concrete keys and need no
wildcard.
Nearest ancestor of `path` present in `boundary-paths`. View paths are finer grained than DOM elements — `[:items 2 :text]` addresses a text node — so a changed path must resolve up to the element that owns it. Falls back to `[]`, the component root. `:*` in a declared path still matches any key, which keeps hand-declared boundary sets working; extracted boundaries name concrete keys and need no wildcard.
(validate boundaries)Returns a seq of problems with an extracted boundary map, empty if sound.
Catches the failure phase 2 hit silently: a boundary with no :id yields a
fragment nothing can target, so a patch for it disappears with no error. This
turns that into something a test or a dev-mode check can assert on.
Returns a seq of problems with an extracted boundary map, empty if sound. Catches the failure phase 2 hit silently: a boundary with no `:id` yields a fragment nothing can target, so a patch for it disappears with no error. This turns that into something a test or a dev-mode check can assert on.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |