@zio.dev/zio-blocks 0.0.33 → 0.0.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/guides/compile-time-resource-safety-with-scope.md +16 -17
- package/guides/getting-started-with-mux.md +1507 -0
- package/guides/query-dsl-extending.md +161 -102
- package/guides/query-dsl-fluent-builder.md +217 -157
- package/guides/query-dsl-reified-optics.md +12 -10
- package/guides/query-dsl-sql.md +246 -165
- package/guides/telemetry-guide.md +1069 -0
- package/guides/zio-schema-migration.md +29 -22
- package/index.md +292 -50
- package/package.json +1 -1
- package/plans/config-follow-up-prs.md +188 -0
- package/plans/config-pr-assessment-roadmap.md +310 -0
- package/reference/MuxDataFlow.jsx +250 -0
- package/reference/async.md +651 -0
- package/reference/chunk.md +3533 -308
- package/reference/codegen/case-class.md +436 -0
- package/reference/codegen/emitter-config.md +383 -0
- package/reference/codegen/examples.md +664 -0
- package/reference/codegen/field.md +316 -0
- package/reference/codegen/index.md +317 -0
- package/reference/codegen/scala-emitter.md +392 -0
- package/reference/codegen/scala-file.md +276 -0
- package/reference/codegen/sealed-trait.md +408 -0
- package/reference/codegen/type-definition.md +340 -0
- package/reference/codegen/type-ref.md +201 -0
- package/reference/combinators.md +347 -117
- package/reference/config.md +158 -0
- package/reference/context.md +4 -4
- package/reference/datastar.md +346 -0
- package/reference/docs.md +1461 -345
- package/reference/endpoint/auth-type.md +146 -0
- package/reference/endpoint/endpoint.md +297 -0
- package/reference/endpoint/http-codec.md +249 -0
- package/reference/endpoint/index.md +825 -0
- package/reference/endpoint/path-codec.md +237 -0
- package/reference/endpoint/route-pattern.md +196 -0
- package/reference/endpoint/route-tree.md +111 -0
- package/reference/endpoint/segment-codec.md +212 -0
- package/reference/html.md +1120 -0
- package/reference/htmx/attribute-values.md +359 -0
- package/reference/htmx/hx-encoding.md +111 -0
- package/reference/htmx/hx-params.md +204 -0
- package/reference/htmx/hx-swap.md +276 -0
- package/reference/htmx/hx-sync.md +251 -0
- package/reference/htmx/hx-target.md +314 -0
- package/reference/htmx/hx-trigger.md +457 -0
- package/reference/htmx/hx-url-update.md +239 -0
- package/reference/htmx/index.md +855 -0
- package/reference/http-model/index.md +47 -0
- package/reference/http-model/model.md +1481 -0
- package/reference/http-model/schema.md +747 -0
- package/reference/maybe.md +826 -0
- package/reference/media-type.md +2 -2
- package/reference/mux.mdx +823 -0
- package/reference/openapi.md +1351 -0
- package/reference/resource-management/defer-handle.md +1 -1
- package/reference/resource-management/resource.md +31 -2
- package/reference/resource-management/scope.md +28 -12
- package/reference/resource-management/wire.md +3 -7
- package/reference/ringbuffer/MpmcDiagram.jsx +717 -0
- package/reference/ringbuffer/MpscDiagram.jsx +618 -0
- package/reference/ringbuffer/SpmcDiagram.jsx +680 -0
- package/reference/ringbuffer/SpscDiagram.jsx +677 -0
- package/reference/ringbuffer/advanced.mdx +109 -0
- package/reference/ringbuffer/index.mdx +145 -0
- package/reference/ringbuffer/mpmc.mdx +151 -0
- package/reference/ringbuffer/mpsc.mdx +132 -0
- package/reference/ringbuffer/spmc.mdx +108 -0
- package/reference/ringbuffer/spsc.mdx +344 -0
- package/reference/{allows.md → schema/allows.md} +4 -4
- package/reference/{binding-resolver.md → schema/binding-resolver.md} +1 -1
- package/reference/{binding.md → schema/binding.md} +2 -3
- package/reference/schema/built-in-codecs/avro.md +451 -0
- package/reference/schema/built-in-codecs/bson.md +480 -0
- package/reference/schema/built-in-codecs/csv.md +564 -0
- package/reference/schema/built-in-codecs/index.md +77 -0
- package/reference/schema/built-in-codecs/json/index.md +295 -0
- package/reference/schema/built-in-codecs/json/json-config.md +217 -0
- package/reference/{json-patch.md → schema/built-in-codecs/json/json-patch.md} +5 -5
- package/reference/{json-schema.md → schema/built-in-codecs/json/json-schema.md} +14 -47
- package/reference/schema/built-in-codecs/json/json-selection.md +322 -0
- package/reference/{json.md → schema/built-in-codecs/json/json.md} +32 -64
- package/reference/schema/built-in-codecs/messagepack.md +508 -0
- package/reference/schema/built-in-codecs/thrift.md +433 -0
- package/reference/schema/built-in-codecs/toon.md +1078 -0
- package/reference/{xml.md → schema/built-in-codecs/xml.md} +13 -9
- package/reference/schema/built-in-codecs/yaml.md +552 -0
- package/reference/{codec.md → schema/codec.md} +10 -10
- package/reference/{dynamic-optic.md → schema/dynamic-optic.md} +151 -5
- package/reference/{dynamic-schema.md → schema/dynamic-schema.md} +8 -8
- package/reference/schema/format.md +92 -0
- package/reference/schema/index.md +50 -0
- package/reference/schema/migration.md +297 -0
- package/reference/{modifier.md → schema/modifier.md} +58 -7
- package/reference/{optics.md → schema/optics.md} +2 -2
- package/reference/{patch.md → schema/patch.md} +1 -1
- package/{path-interpolator.md → reference/schema/path-interpolator.md} +165 -72
- package/reference/{schema-evolution → schema/schema-evolution}/as.md +8 -8
- package/reference/{schema-evolution → schema/schema-evolution}/index.md +2 -2
- package/reference/{schema-evolution → schema/schema-evolution}/into.md +8 -8
- package/reference/{schema-expr.md → schema/schema-expr.md} +110 -175
- package/reference/{schema.md → schema/schema.md} +12 -0
- package/reference/{structural-types.md → schema/structural-types.md} +1 -1
- package/reference/{type-class-derivation.md → schema/type-class-derivation.md} +63 -1
- package/reference/smithy.md +533 -0
- package/reference/sql/db-codec-deriver.md +71 -0
- package/reference/sql/db-codec.md +687 -0
- package/reference/sql/db-con.md +271 -0
- package/reference/sql/db-connection.md +153 -0
- package/reference/sql/db-param-writer.md +77 -0
- package/reference/sql/db-param.md +66 -0
- package/reference/sql/db-result-reader.md +146 -0
- package/reference/sql/db-tx.md +82 -0
- package/reference/sql/db-value.md +41 -0
- package/reference/sql/ddl.md +85 -0
- package/reference/sql/frag.md +254 -0
- package/reference/sql/index.md +341 -0
- package/reference/sql/repo.md +600 -0
- package/reference/sql/sql-dialect.md +73 -0
- package/reference/sql/sql-logger.md +62 -0
- package/reference/sql/sql-name-mapper.md +70 -0
- package/reference/sql/table-metadata.md +134 -0
- package/reference/sql/table.md +448 -0
- package/reference/sql/transactor-zio.md +399 -0
- package/reference/sql/transactor.md +353 -0
- package/reference/sql-zio.md +112 -0
- package/reference/streams/concurrent-operators.md +106 -0
- package/reference/streams/index.md +653 -0
- package/reference/streams/pipeline.md +718 -0
- package/reference/streams/reader.md +1284 -0
- package/reference/streams/scala-2-compatibility.md +55 -0
- package/reference/streams/sink.md +1426 -0
- package/reference/streams/stream.md +2526 -0
- package/reference/streams/writer.md +1045 -0
- package/reference/streams/zero-boxing.md +275 -0
- package/reference/telemetry.md +693 -0
- package/reference/typeid.md +5 -19
- package/sidebars.js +238 -43
- package/reference/formats.md +0 -694
- package/reference/http-model.md +0 -1716
- package/reference/streams.md +0 -989
- package/ringbuffer.md +0 -249
- /package/reference/{json-differ.md → schema/built-in-codecs/json/json-differ.md} +0 -0
- /package/reference/{dynamic-value.md → schema/dynamic-value.md} +0 -0
- /package/reference/{lazy.md → schema/lazy.md} +0 -0
- /package/reference/{reflect.md → schema/reflect.md} +0 -0
- /package/reference/{registers.md → schema/registers.md} +0 -0
- /package/reference/{schema-error.md → schema/schema-error.md} +0 -0
- /package/reference/{syntax.md → schema/syntax.md} +0 -0
- /package/reference/{validation.md → schema/validation.md} +0 -0
|
@@ -0,0 +1,1120 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: html
|
|
3
|
+
title: "HTML"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
`zio-blocks-html` is a **type-safe HTML templating library** providing immutable data structures and a fluent DSL for building HTML, CSS, and JavaScript. It offers compile-time safety, automatic XSS protection, and zero-dependency simplicity across Scala 2.13, 3.x, and both JVM and Scala.js platforms.
|
|
7
|
+
|
|
8
|
+
Core types: `Dom` (HTML tree), `CssSelector` (CSS queries), `DomSelection` (DOM navigation), `Css` (stylesheets), `Js` (JavaScript expressions).
|
|
9
|
+
|
|
10
|
+
The module is structured around these core types:
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
import zio.blocks.html._
|
|
14
|
+
import zio.blocks.chunk.Chunk
|
|
15
|
+
|
|
16
|
+
// Dom variants
|
|
17
|
+
sealed trait Dom
|
|
18
|
+
case class Dom.Text(content: String) extends Dom
|
|
19
|
+
sealed trait Dom.Element extends Dom
|
|
20
|
+
case class Dom.Element.Generic(tag: String, attributes: Chunk[Dom.Attribute], children: Chunk[Dom]) extends Dom.Element
|
|
21
|
+
case class Dom.Element.Script(attributes: Chunk[Dom.Attribute], children: Chunk[Dom]) extends Dom.Element
|
|
22
|
+
case class Dom.Element.Style(attributes: Chunk[Dom.Attribute], children: Chunk[Dom]) extends Dom.Element
|
|
23
|
+
|
|
24
|
+
// Attribute variants
|
|
25
|
+
sealed trait Dom.Attribute
|
|
26
|
+
case class Dom.Attribute.KeyValue(name: String, value: Dom.AttributeValue) extends Dom.Attribute
|
|
27
|
+
case class Dom.Attribute.AppendValue(name: String, value: Dom.AttributeValue, separator: Dom.AttributeSeparator) extends Dom.Attribute
|
|
28
|
+
case class Dom.Attribute.BooleanAttribute(name: String, enabled: Boolean) extends Dom.Attribute
|
|
29
|
+
|
|
30
|
+
// CssSelector variants (additional types omitted for brevity)
|
|
31
|
+
sealed trait CssSelector
|
|
32
|
+
case class CssSelector.Element(tag: String) extends CssSelector
|
|
33
|
+
case class CssSelector.Class(name: String) extends CssSelector
|
|
34
|
+
|
|
35
|
+
// Css variants
|
|
36
|
+
sealed trait Css
|
|
37
|
+
case class Css.Rule(selector: CssSelector, declarations: Chunk[Css.Declaration]) extends Css
|
|
38
|
+
case class Css.Declaration(property: String, value: String) extends Css
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Motivation
|
|
42
|
+
|
|
43
|
+
Why use `zio-blocks-html`?
|
|
44
|
+
|
|
45
|
+
String concatenation for HTML is error-prone:
|
|
46
|
+
- No type safety — typos in tag names or attributes only surface at runtime
|
|
47
|
+
- Manual escaping is tedious and easy to forget
|
|
48
|
+
- Tightly couples HTML structure to data serialization
|
|
49
|
+
|
|
50
|
+
Template engines with runtime parsing add overhead and complexity:
|
|
51
|
+
- Parsing validation on every render
|
|
52
|
+
- Separate template syntax to learn
|
|
53
|
+
- Difficult to compose programmatically
|
|
54
|
+
|
|
55
|
+
`zio-blocks-html` provides:
|
|
56
|
+
- **Type-safe construction** via DSL functions (`div(id := "main", p("Hello"))`)
|
|
57
|
+
- **Automatic XSS protection** through position-aware typeclasses and HTML escaping
|
|
58
|
+
- **Scala 3 compile-time optimizations** for string interpolators (constant folding, macro validation)
|
|
59
|
+
- **Zero dependencies** — pure Scala, works with any HTTP framework
|
|
60
|
+
- **Cross-platform** — identical API on JVM and Scala.js
|
|
61
|
+
- **Structured querying** — CSS selectors for DOM navigation and testing
|
|
62
|
+
|
|
63
|
+
## Installation
|
|
64
|
+
|
|
65
|
+
Add to `build.sbt`:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
libraryDependencies += "dev.zio" %% "zio-blocks-html" % "0.0.51"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
For Scala.js projects, use `%%%`:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
libraryDependencies += "dev.zio" %%% "zio-blocks-html" % "0.0.51"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Supported Scala versions: 2.13.x and 3.x
|
|
78
|
+
|
|
79
|
+
:::note
|
|
80
|
+
The `html` module depends transitively on `zio-blocks-schema` and `zio-blocks-chunk`. The schema dependency enables `ToJs` auto-derivation via JSON encoding — if your type has a `Schema` instance, it automatically becomes usable in `js"..."` expressions.
|
|
81
|
+
:::
|
|
82
|
+
|
|
83
|
+
## Overview: How They Work Together
|
|
84
|
+
|
|
85
|
+
The module is organized around five core subsystems that compose together:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
┌─────────────────────────────────────────────────────┐
|
|
89
|
+
│ DSL Functions (div, p, span, ...) │
|
|
90
|
+
│ + Attribute Builders (id :=, className +=) │
|
|
91
|
+
│ └─> Produces: Dom.Element.Generic │
|
|
92
|
+
│ │
|
|
93
|
+
├─────────────────────────────────────────────────────┤
|
|
94
|
+
│ String Interpolators (html"", css"", js"") │
|
|
95
|
+
│ └─> Position-aware typeclasses │
|
|
96
|
+
│ └─> html"" → Dom.Element (attribute escaping) │
|
|
97
|
+
│ └─> css"" → Css (CSS value types) │
|
|
98
|
+
│ └─> js"" → Js (JavaScript with </script> guard) │
|
|
99
|
+
│ │
|
|
100
|
+
├─────────────────────────────────────────────────────┤
|
|
101
|
+
│ Dom ADT (sealed trait Dom) │
|
|
102
|
+
│ ├─> Dom.Text (HTML-escaped text content) │
|
|
103
|
+
│ ├─> Dom.Element (Generic, Script, Style) │
|
|
104
|
+
│ ├─> Dom.Doctype (<!DOCTYPE html>) │
|
|
105
|
+
│ └─> Dom.Empty (renders to nothing) │
|
|
106
|
+
│ │
|
|
107
|
+
├─────────────────────────────────────────────────────┤
|
|
108
|
+
│ Rendering │
|
|
109
|
+
│ ├─> dom.render → minified HTML String │
|
|
110
|
+
│ ├─> dom.render(indent=2) → pretty-printed String │
|
|
111
|
+
│ └─> Used in HTTP responses, testing │
|
|
112
|
+
│ │
|
|
113
|
+
├─────────────────────────────────────────────────────┤
|
|
114
|
+
│ CSS Selectors + DOM Querying │
|
|
115
|
+
│ ├─> CssSelector ADT (Element, Class, Id, etc.) │
|
|
116
|
+
│ ├─> Fluent combinators (>, >>, +, ~, &, |) │
|
|
117
|
+
│ ├─> DomSelection API (select, filter, extract) │
|
|
118
|
+
│ └─> Used for testing, transformation, navigation │
|
|
119
|
+
└─────────────────────────────────────────────────────┘
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**Typical workflow:**
|
|
123
|
+
|
|
124
|
+
1. **Build** — use DSL (`div(className := "card", ...)`) or interpolators (`html"<div>$content</div>"`)
|
|
125
|
+
2. **Compose** — nest elements and use typeclasses to extend for custom types
|
|
126
|
+
3. **Query** — use CSS selectors (`page.select(div.hover).texts`) for testing or transformation
|
|
127
|
+
4. **Render** — call `Dom#render` or `Dom#render(indent)` for HTTP response or testing
|
|
128
|
+
5. **Style** — use `css"..."` and `Css.Rule` to define or embed stylesheets
|
|
129
|
+
|
|
130
|
+
## The Dom ADT
|
|
131
|
+
|
|
132
|
+
The `Dom` sealed trait is the core data model. Everything in the module works with or produces `Dom` nodes.
|
|
133
|
+
|
|
134
|
+
### The Four Node Types
|
|
135
|
+
|
|
136
|
+
A `Dom` tree is composed of four node types:
|
|
137
|
+
|
|
138
|
+
#### Dom.Text
|
|
139
|
+
|
|
140
|
+
A text node containing string content. Text is automatically HTML-escaped when rendered to prevent XSS injection. Escaping happens at render time (not construction time), so `Dom.Text` stores the raw string.
|
|
141
|
+
|
|
142
|
+
To create text nodes, use the DSL (strings are converted via `ToModifier[String]`) or explicitly. Call `Dom#render` to convert a text node to an HTML string:
|
|
143
|
+
|
|
144
|
+
```scala
|
|
145
|
+
import zio.blocks.html._
|
|
146
|
+
|
|
147
|
+
val text = Dom.Text("Hello, world!")
|
|
148
|
+
// text: Text = Text("Hello, world!")
|
|
149
|
+
text.render
|
|
150
|
+
// res0: String = "Hello, world!"
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
#### Dom.Element.Generic
|
|
154
|
+
|
|
155
|
+
A standard HTML element. The tag is the element name, attributes is a `Chunk` of key-value pairs and modifiers, and children is a `Chunk` of DOM nodes. Text content in children is HTML-escaped during rendering.
|
|
156
|
+
|
|
157
|
+
The DSL functions (`div`, `p`, `span`, etc.) construct these:
|
|
158
|
+
|
|
159
|
+
```scala
|
|
160
|
+
import zio.blocks.html._
|
|
161
|
+
|
|
162
|
+
val elem = div(id := "main", p("Content"))
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
#### Dom.Element.Script
|
|
166
|
+
|
|
167
|
+
A specialized script element with `tag = "script"`. Unlike `Generic`, Script renders its text children **without HTML escaping**, allowing inline JavaScript to be emitted as-is. This is the mechanism that enables safe `js"..."` interpolation — the interpolator escapes the JavaScript, but the Script element does not double-escape.
|
|
168
|
+
|
|
169
|
+
Use `script().inlineJs(js"...")` or `script().externalJs(url)`:
|
|
170
|
+
|
|
171
|
+
```scala
|
|
172
|
+
import zio.blocks.html._
|
|
173
|
+
|
|
174
|
+
val inlineScript = script().inlineJs(js"console.log('Hello');")
|
|
175
|
+
val externalScript = script().externalJs("/app.js")
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
#### Dom.Element.Style
|
|
179
|
+
|
|
180
|
+
A specialized style element with `tag = "style"`. Like Script, Style renders text children **without escaping**, allowing raw CSS to be emitted. Use `style().inlineCss(css"...")`:
|
|
181
|
+
|
|
182
|
+
```scala
|
|
183
|
+
import zio.blocks.html._
|
|
184
|
+
|
|
185
|
+
val inlineStyle = style().inlineCss(css"body { margin: 0; }")
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
#### Dom.Doctype
|
|
189
|
+
|
|
190
|
+
A DOCTYPE declaration node. Renders as `<!DOCTYPE value>`. The singleton `doctype` value renders as `<!DOCTYPE html>`:
|
|
191
|
+
|
|
192
|
+
```scala
|
|
193
|
+
import zio.blocks.html._
|
|
194
|
+
|
|
195
|
+
doctype.render
|
|
196
|
+
// res4: String = "<!DOCTYPE html>"
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
#### Dom.Empty
|
|
200
|
+
|
|
201
|
+
A no-op node that renders to empty string. Useful for conditional rendering (e.g., `if (condition) element else Dom.Empty`).
|
|
202
|
+
|
|
203
|
+
### Attributes and Values
|
|
204
|
+
|
|
205
|
+
Elements carry a `Chunk[Dom.Attribute]` where each attribute is one of:
|
|
206
|
+
|
|
207
|
+
- **`Dom.Attribute.KeyValue(name: String, value: Dom.AttributeValue)`** — A standard `name="value"` attribute. The value can be a string, multi-value, boolean, or JavaScript expression.
|
|
208
|
+
|
|
209
|
+
- **`Dom.Attribute.AppendValue(name: String, value: Dom.AttributeValue, separator: Dom.AttributeSeparator)`** — Declares a value to be concatenated with any existing value for the same attribute. Provides accumulation of multi-valued attributes like `class` and `rel`. The separator appears between values (Space, Comma, Semicolon, or Custom).
|
|
210
|
+
|
|
211
|
+
- **`Dom.Attribute.BooleanAttribute(name: String, enabled: Boolean)`** — A standalone attribute (like `disabled`, `checked`, `required`) that renders as just the name when enabled, or is omitted when disabled.
|
|
212
|
+
|
|
213
|
+
:::tip
|
|
214
|
+
The DSL provides `id := value` (`:=`) for single-valued attributes and `className += "class"` (`+=`) for appending to multi-valued attributes. Both are easier to use than constructing `Dom.Attribute` directly.
|
|
215
|
+
:::
|
|
216
|
+
|
|
217
|
+
### Tree Traversal Operations
|
|
218
|
+
|
|
219
|
+
`Dom` provides four pure tree transformation methods:
|
|
220
|
+
|
|
221
|
+
**`Dom#collect(pf: PartialFunction[Dom, Dom]): List[Dom]`** — Applies a partial function to every node in the tree (depth-first) and collects the results. Useful for extracting or transforming specific nodes:
|
|
222
|
+
|
|
223
|
+
```scala
|
|
224
|
+
import zio.blocks.html._
|
|
225
|
+
|
|
226
|
+
val tree = div(p("A"), span("B"), p("C"))
|
|
227
|
+
val paragraphs = tree.collect { case el: Dom.Element if el.tag == "p" => el }
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
**`Dom#filter(predicate: Dom => Boolean): Dom`** — Removes any node for which the predicate returns false. Non-matching nodes are replaced with `Dom.Empty`, and their children are lost. Matching elements have their children recursively filtered:
|
|
231
|
+
|
|
232
|
+
```scala
|
|
233
|
+
import zio.blocks.html._
|
|
234
|
+
|
|
235
|
+
val tree = div(p("A"), span("Keep"), p("B"), span("Also keep"))
|
|
236
|
+
val filtered = tree.filter {
|
|
237
|
+
case el: Dom.Element => el.tag == "div" || el.tag == "span"
|
|
238
|
+
case _ => true
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**`Dom#find(predicate: Dom => Boolean): Option[Dom]`** — Returns the first node (depth-first) matching the predicate, or `None`:
|
|
243
|
+
|
|
244
|
+
```scala
|
|
245
|
+
import zio.blocks.html._
|
|
246
|
+
|
|
247
|
+
val tree = div(p("First"), span("Second"), p("Third"))
|
|
248
|
+
tree.find { case el: Dom.Element => el.tag == "p"; case _ => false }
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**`Dom#transform(f: Dom => Dom): Dom`** — Applies a transformation function to every node in pre-order. Each node receives the transformation first, and if it is an Element, its children are recursed on the transformed node, so a transformation that changes the child list affects what gets recursed into:
|
|
252
|
+
|
|
253
|
+
```scala
|
|
254
|
+
import zio.blocks.html._
|
|
255
|
+
|
|
256
|
+
val tree = div(h3("Old"), p("Content"))
|
|
257
|
+
val upgraded = tree.transform {
|
|
258
|
+
case el: Dom.Element.Generic if el.tag == "h3" => el.copy(tag = "h2")
|
|
259
|
+
case other => other
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## The HTML DSL
|
|
264
|
+
|
|
265
|
+
The DSL provides functions for all HTML5 elements, allowing fluent construction with a modifier pattern.
|
|
266
|
+
|
|
267
|
+
### Element Functions
|
|
268
|
+
|
|
269
|
+
All standard HTML elements are available as lowercase functions (or backtick-quoted if they shadow Scala keywords):
|
|
270
|
+
|
|
271
|
+
```scala
|
|
272
|
+
import zio.blocks.html._
|
|
273
|
+
|
|
274
|
+
// Basic elements
|
|
275
|
+
val paragraph = p("Hello, world!")
|
|
276
|
+
val heading = h1("Welcome")
|
|
277
|
+
val container = div("Content here")
|
|
278
|
+
|
|
279
|
+
// Void elements (auto self-closing)
|
|
280
|
+
val image = img(src := "photo.jpg", alt := "A photo")
|
|
281
|
+
val lineBreak = br
|
|
282
|
+
val horizontalRule = hr
|
|
283
|
+
|
|
284
|
+
// Keyword-shadowed elements (use backticks)
|
|
285
|
+
val objElement = `object`("data.bin")
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Setting Attributes
|
|
289
|
+
|
|
290
|
+
Attributes are set using the `:=` operator on `AttributeKey` builders:
|
|
291
|
+
|
|
292
|
+
```scala
|
|
293
|
+
import zio.blocks.html._
|
|
294
|
+
|
|
295
|
+
val link = a(
|
|
296
|
+
href := "https://example.com",
|
|
297
|
+
target := "_blank",
|
|
298
|
+
titleAttr := "Visit Example",
|
|
299
|
+
"Visit Example"
|
|
300
|
+
)
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Multi-Valued Attributes: Override vs Accumulate
|
|
304
|
+
|
|
305
|
+
For attributes that can have multiple values (`class`, `rel`, `accept`), you can override or accumulate:
|
|
306
|
+
|
|
307
|
+
- **`:=` (override)** — Replaces any previous value. Last assignment wins:
|
|
308
|
+
|
|
309
|
+
```scala
|
|
310
|
+
import zio.blocks.html._
|
|
311
|
+
|
|
312
|
+
val div1 = div(className := "a", className := "b")
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
- **`+=` (append)** — Concatenates with the previous value using a separator (space for `class`):
|
|
316
|
+
|
|
317
|
+
```scala
|
|
318
|
+
import zio.blocks.html._
|
|
319
|
+
|
|
320
|
+
val div2 = div(className += "card", className += "active")
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
- **Mixed** — Set a base value, then append:
|
|
324
|
+
|
|
325
|
+
```scala
|
|
326
|
+
import zio.blocks.html._
|
|
327
|
+
|
|
328
|
+
val div3 = div(className := "base").when(true)(className += "extra")
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### Boolean Attributes
|
|
332
|
+
|
|
333
|
+
Use `BooleanAttribute.:=` to conditionally include attributes like `disabled`, `required`, or `checked`:
|
|
334
|
+
|
|
335
|
+
```scala
|
|
336
|
+
import zio.blocks.html._
|
|
337
|
+
|
|
338
|
+
val isDisabled = true
|
|
339
|
+
val submitBtn = button(disabled := isDisabled, "Submit")
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### Data and ARIA Attributes
|
|
343
|
+
|
|
344
|
+
Use `dataAttr(name)` and `aria(name)` builders for HTML5 data attributes and ARIA accessibility attributes:
|
|
345
|
+
|
|
346
|
+
```scala
|
|
347
|
+
import zio.blocks.html._
|
|
348
|
+
|
|
349
|
+
val userDiv = div(
|
|
350
|
+
dataAttr("user-id") := "42",
|
|
351
|
+
dataAttr("action") := "edit",
|
|
352
|
+
"User Card"
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
val closeBtn = button(
|
|
356
|
+
aria("label") := "Close",
|
|
357
|
+
aria("expanded") := "false",
|
|
358
|
+
"×"
|
|
359
|
+
)
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
For custom attributes not provided by the DSL, use the generic `attr(name)` builder:
|
|
363
|
+
|
|
364
|
+
```scala
|
|
365
|
+
import zio.blocks.html._
|
|
366
|
+
|
|
367
|
+
val alpineDiv = div(attr("x-data") := "{open: false}", "Alpine.js component")
|
|
368
|
+
val htmxDiv = div(attr("hx-get") := "/api/data", "HTMX target")
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### Programmatic Multi-Valued Attributes
|
|
372
|
+
|
|
373
|
+
For programmatic construction of multi-valued attributes (when the DSL `+=` operator is not available), use `Dom.multiAttr(name)` or `Dom.multiAttr(name, separator)`:
|
|
374
|
+
|
|
375
|
+
```scala
|
|
376
|
+
import zio.blocks.html._
|
|
377
|
+
|
|
378
|
+
// Create a custom multi-valued attribute builder (alternative to DSL's className)
|
|
379
|
+
val classBuilder = Dom.multiAttr("class")
|
|
380
|
+
val div1 = div(classBuilder := "base", classBuilder += "active")
|
|
381
|
+
|
|
382
|
+
// Create a multi-valued attribute with explicit separator
|
|
383
|
+
val rel = Dom.multiAttr("rel", Dom.AttributeSeparator.Space)
|
|
384
|
+
val link = a(rel := "prev", rel += "first", href := "/previous")
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
The `MultiAttributeKey` class handles accumulation of values with configurable separators (Space, Comma, Semicolon, or Custom).
|
|
388
|
+
|
|
389
|
+
For constructing multi-valued attributes directly from collections (without a builder chain), use the `Iterable[String]` overload:
|
|
390
|
+
|
|
391
|
+
```scala
|
|
392
|
+
import zio.blocks.html._
|
|
393
|
+
|
|
394
|
+
// Directly create a multi-valued attribute from a collection
|
|
395
|
+
val customClasses = Dom.multiAttr("class", List("card", "active", "large"))
|
|
396
|
+
val div1 = div(customClasses)
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
This approach is useful when programmatically building multi-valued attributes outside the DSL's builder pattern.
|
|
400
|
+
|
|
401
|
+
### Children
|
|
402
|
+
|
|
403
|
+
Children can be strings, elements, or collections. The DSL uses the `ToModifier` typeclass to convert values into DOM nodes:
|
|
404
|
+
|
|
405
|
+
```scala
|
|
406
|
+
import zio.blocks.html._
|
|
407
|
+
|
|
408
|
+
// String children are converted to Dom.Text
|
|
409
|
+
val simple = p("Plain text")
|
|
410
|
+
|
|
411
|
+
// Element children are nested
|
|
412
|
+
val nested = div(p("First"), p("Second"))
|
|
413
|
+
|
|
414
|
+
// Lists of children — elements append directly, no wrapper
|
|
415
|
+
val items = List("Apple", "Banana", "Cherry")
|
|
416
|
+
val listEl = ul(items.map(item => li(item)))
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### Conditional Rendering
|
|
420
|
+
|
|
421
|
+
Use `when(condition)` to apply modifiers conditionally:
|
|
422
|
+
|
|
423
|
+
```scala
|
|
424
|
+
import zio.blocks.html._
|
|
425
|
+
|
|
426
|
+
val isHighlighted = true
|
|
427
|
+
val box = div(
|
|
428
|
+
className := "box"
|
|
429
|
+
).when(isHighlighted)(
|
|
430
|
+
className += "highlighted",
|
|
431
|
+
titleAttr := "This is highlighted"
|
|
432
|
+
)
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
Use `whenSome(option)` to apply modifiers based on an `Option`:
|
|
436
|
+
|
|
437
|
+
```scala
|
|
438
|
+
import zio.blocks.html._
|
|
439
|
+
|
|
440
|
+
val maybeTitle: Option[String] = Some("Important")
|
|
441
|
+
val card = div(
|
|
442
|
+
className := "card",
|
|
443
|
+
p("Content")
|
|
444
|
+
).whenSome(maybeTitle)(t => Seq(
|
|
445
|
+
titleAttr := t,
|
|
446
|
+
className += "has-title"
|
|
447
|
+
))
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
### Void Elements
|
|
451
|
+
|
|
452
|
+
Void elements (self-closing tags) automatically render with the correct syntax:
|
|
453
|
+
|
|
454
|
+
```scala
|
|
455
|
+
import zio.blocks.html._
|
|
456
|
+
|
|
457
|
+
val voidElements = div(
|
|
458
|
+
br,
|
|
459
|
+
hr,
|
|
460
|
+
img(src := "photo.jpg", alt := "A photo"),
|
|
461
|
+
input(`type` := "text", placeholder := "Enter text")
|
|
462
|
+
)
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
## String Interpolators
|
|
466
|
+
|
|
467
|
+
The module provides four string interpolators: `html""`, `css""`, `js""`, and `selector""`. All offer compile-time safety (on Scala 3) and automatic escaping appropriate to their context.
|
|
468
|
+
|
|
469
|
+
### `html""` Interpolator
|
|
470
|
+
|
|
471
|
+
The `html""` interpolator is **position-aware**: it detects whether each interpolated argument is in an attribute-value position or content position, then summons the appropriate typeclass.
|
|
472
|
+
|
|
473
|
+
The `html""` interpolator determines position based on the preceding string:
|
|
474
|
+
|
|
475
|
+
**Position detection:**
|
|
476
|
+
- **Attribute position**: if the preceding string part ends with `=`, `='`, or `="`, the next argument is treated as an attribute value and uses `ToAttrValue[A]`
|
|
477
|
+
- **Content position**: otherwise, the argument is treated as element content and uses `ToElements[A]`
|
|
478
|
+
|
|
479
|
+
Position-aware interpolation enables safe attribute values and content:
|
|
480
|
+
|
|
481
|
+
```scala
|
|
482
|
+
import zio.blocks.html._
|
|
483
|
+
|
|
484
|
+
val name = "Alice"
|
|
485
|
+
val age = 30
|
|
486
|
+
|
|
487
|
+
// name is in content position → ToElements[String]
|
|
488
|
+
// age is in attribute position → ToAttrValue[Int]
|
|
489
|
+
val elem = html"""<div id="user-$age" class="profile">User: $name</div>"""
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
**Single-root requirement**: The `html""` interpolator requires a **single root element**. Multiple top-level nodes cause an exception at runtime (Scala 2) or compile error for static templates (Scala 3):
|
|
493
|
+
|
|
494
|
+
```scala
|
|
495
|
+
import zio.blocks.html._
|
|
496
|
+
|
|
497
|
+
// This compiles (single root)
|
|
498
|
+
val page = html"<div><p>A</p><p>B</p></div>"
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
**XSS Protection:**
|
|
502
|
+
|
|
503
|
+
Content interpolated into `html""` is stored as `Dom.Text` nodes, which are HTML-escaped during `Dom#render` to prevent XSS:
|
|
504
|
+
|
|
505
|
+
```scala
|
|
506
|
+
import zio.blocks.html._
|
|
507
|
+
|
|
508
|
+
val userInput = "<script>alert('XSS')</script>"
|
|
509
|
+
val safe = html"<p>$userInput</p>"
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
:::danger
|
|
513
|
+
The `html""` interpolator requires a **single root element**. On Scala 3, pure-static templates with multiple roots fail at compile time. On Scala 2 or for dynamic templates, the error occurs at evaluation time. Always wrap multiple elements in a container (e.g., `<div>`).
|
|
514
|
+
:::
|
|
515
|
+
|
|
516
|
+
### `css""` Interpolator
|
|
517
|
+
|
|
518
|
+
The `css""` interpolator returns a `Css` value with `ToCss` typeclass dispatch for interpolated parts:
|
|
519
|
+
|
|
520
|
+
```scala
|
|
521
|
+
import zio.blocks.html._
|
|
522
|
+
|
|
523
|
+
val color = "blue"
|
|
524
|
+
val size = 16
|
|
525
|
+
|
|
526
|
+
val styles = css"color: $color; font-size: ${size}px;"
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
Use `CssLength` and `CssColor` types for type-safe CSS values:
|
|
530
|
+
|
|
531
|
+
```scala
|
|
532
|
+
import zio.blocks.html._
|
|
533
|
+
|
|
534
|
+
val width = css"width: ${CssLength(300.0, "px")};"
|
|
535
|
+
val background = css"background: ${CssColor.Hex.unsafe("ff0000")};"
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
:::note
|
|
539
|
+
`CssColor.Hex` returns `Option[CssColor]` because hex validation may fail. Use `CssColor.Hex.apply()` (or just `CssColor.Hex(...)`) for safe validation, or `CssColor.Hex.unsafe()` for known-valid hex strings.
|
|
540
|
+
:::
|
|
541
|
+
|
|
542
|
+
### `js""` Interpolator
|
|
543
|
+
|
|
544
|
+
The `js""` interpolator returns a `Js` value. Strings are automatically quoted and escaped; numerics and booleans are rendered unquoted:
|
|
545
|
+
|
|
546
|
+
```scala
|
|
547
|
+
import zio.blocks.html._
|
|
548
|
+
|
|
549
|
+
val message = "Hello, world!"
|
|
550
|
+
val count = 42
|
|
551
|
+
|
|
552
|
+
val code = js"console.log($message); alert($count);"
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
:::warning
|
|
556
|
+
Never interpolate untrusted user input directly into `js""`. The interpolator escapes string values, but the `Js` value is rendered without additional escaping in script elements. Always use `ToJs[String]` (which automatically quotes and escapes) for any untrusted input.
|
|
557
|
+
:::
|
|
558
|
+
|
|
559
|
+
The interpolator protects against `</script>` injection by escaping `<` and `>` as Unicode escapes:
|
|
560
|
+
|
|
561
|
+
```scala
|
|
562
|
+
import zio.blocks.html._
|
|
563
|
+
|
|
564
|
+
val userInput = "if (x < y) alert('<script>');"
|
|
565
|
+
val code = js"let check = $userInput"
|
|
566
|
+
|
|
567
|
+
println(code.value)
|
|
568
|
+
// let check = "if (x < y) alert('<script>');"
|
|
569
|
+
// (with < and > characters escaped as < and > in actual output)
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
### `selector""` Interpolator
|
|
573
|
+
|
|
574
|
+
The `selector""` interpolator returns a `CssSelector`:
|
|
575
|
+
|
|
576
|
+
```scala
|
|
577
|
+
import zio.blocks.html._
|
|
578
|
+
|
|
579
|
+
val className = "active"
|
|
580
|
+
val selector = selector".$className"
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
## CSS Selectors and the DSL
|
|
584
|
+
|
|
585
|
+
The `CssSelector` ADT provides a fluent DSL for building CSS selectors with combinator methods.
|
|
586
|
+
|
|
587
|
+
### Basic Selectors
|
|
588
|
+
|
|
589
|
+
Create selectors for elements, classes, IDs, or universal matches:
|
|
590
|
+
|
|
591
|
+
```scala
|
|
592
|
+
import zio.blocks.html._
|
|
593
|
+
|
|
594
|
+
val divSel = CssSelector.Element("div")
|
|
595
|
+
val classSel = CssSelector.Class("container")
|
|
596
|
+
val idSel = CssSelector.Id("header")
|
|
597
|
+
val universal = CssSelector.Universal
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
### Combinators
|
|
601
|
+
|
|
602
|
+
All HTML elements (`div`, `span`, `p`, etc.) implement `CssSelectable`, so you can use them directly as selectors:
|
|
603
|
+
|
|
604
|
+
```scala
|
|
605
|
+
import zio.blocks.html._
|
|
606
|
+
|
|
607
|
+
// Element selectors via DSL elements
|
|
608
|
+
val divSel = div.selector
|
|
609
|
+
val childSel = div > span // div > span
|
|
610
|
+
val descendantSel = div >> span // div span (descendant)
|
|
611
|
+
val adjacentSel = div + span // div + span
|
|
612
|
+
val siblingSel = div ~ span // div ~ span
|
|
613
|
+
val andSel = div & CssSelector.Class("active") // div.active
|
|
614
|
+
val orSel = div | span // div, span
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
### Pseudo-Classes and Pseudo-Elements
|
|
618
|
+
|
|
619
|
+
Pseudo-classes match elements by their state, and pseudo-elements create dynamic content:
|
|
620
|
+
|
|
621
|
+
```scala
|
|
622
|
+
import zio.blocks.html._
|
|
623
|
+
|
|
624
|
+
val hoverSel = a.hover // a:hover
|
|
625
|
+
val firstChild = li.firstChild // li:first-child
|
|
626
|
+
val nthChild = tr.nthChild(2) // tr:nth-child(2)
|
|
627
|
+
val before = div.before // div::before
|
|
628
|
+
val after = span.after // span::after
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
### Attribute Selectors
|
|
632
|
+
|
|
633
|
+
Select elements by their attribute values using built-in matchers:
|
|
634
|
+
|
|
635
|
+
```scala
|
|
636
|
+
import zio.blocks.html._
|
|
637
|
+
|
|
638
|
+
val input = CssSelector.Element("input")
|
|
639
|
+
|
|
640
|
+
val hasType = input.withAttribute("type") // input[type]
|
|
641
|
+
val exactType = input.withAttribute("type", "text") // input[type="text"]
|
|
642
|
+
val containsClass = input.withAttributeContaining("class", "btn") // input[class*="btn"]
|
|
643
|
+
val startsWithHref = a.withAttributeStarting("href", "https") // a[href^="https"]
|
|
644
|
+
val endsWithPng = img.withAttributeEnding("src", ".png") // img[src$=".png"]
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
## DOM Querying with DomSelection
|
|
648
|
+
|
|
649
|
+
The `DomSelection` API lets you query and navigate DOM trees using CSS selectors. It is useful for testing, transforming templates, and extracting information.
|
|
650
|
+
|
|
651
|
+
### Selecting Elements
|
|
652
|
+
|
|
653
|
+
Call `Dom#select(selector)` to query the tree:
|
|
654
|
+
|
|
655
|
+
```scala
|
|
656
|
+
import zio.blocks.html._
|
|
657
|
+
|
|
658
|
+
val page = div(
|
|
659
|
+
header(nav(a(href := "/", "Home"), a(href := "/about", "About"))),
|
|
660
|
+
main(p(className += "intro", "Hello"), p("World")),
|
|
661
|
+
footer(p("© 2026"))
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
// Query by tag
|
|
665
|
+
val paragraphs = page.select(CssSelector.Element("p"))
|
|
666
|
+
paragraphs.length
|
|
667
|
+
|
|
668
|
+
// Query by class
|
|
669
|
+
val intros = page.select(CssSelector.Class("intro"))
|
|
670
|
+
intros.texts
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
### Navigation
|
|
674
|
+
|
|
675
|
+
The `DomSelection` API returned by `Dom#select` provides navigation methods like `.children` and `.first` to traverse the selection results:
|
|
676
|
+
|
|
677
|
+
```scala
|
|
678
|
+
import zio.blocks.html._
|
|
679
|
+
|
|
680
|
+
val page = div(
|
|
681
|
+
nav(a(href := "/", "Home"), a(href := "/about", "About")),
|
|
682
|
+
main(p("Content"))
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
// Direct children
|
|
686
|
+
val navLinks = page.select(CssSelector.Element("nav")).children
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
### Extraction
|
|
690
|
+
|
|
691
|
+
The `DomSelection` API provides methods like `.attrs` and `.texts` to extract attribute values and text content from selected elements:
|
|
692
|
+
|
|
693
|
+
```scala
|
|
694
|
+
import zio.blocks.html._
|
|
695
|
+
|
|
696
|
+
val page = div(
|
|
697
|
+
a(href := "/home", "Home"),
|
|
698
|
+
a(href := "/about", "About"),
|
|
699
|
+
a(href := "/contact", "Contact")
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
val hrefs = page.select(CssSelector.Element("a")).attrs("href")
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
### Filtering
|
|
706
|
+
|
|
707
|
+
The `DomSelection` API provides `.filter` and `.withClass` methods to filter selections by predicate or attribute:
|
|
708
|
+
|
|
709
|
+
```scala
|
|
710
|
+
import zio.blocks.html._
|
|
711
|
+
|
|
712
|
+
val page = div(
|
|
713
|
+
p(className += "visible", "A"),
|
|
714
|
+
p(className += "hidden", "B"),
|
|
715
|
+
p("C")
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
// Filter by predicate
|
|
719
|
+
val visible = page.select(CssSelector.Element("p")).filter {
|
|
720
|
+
case el: Dom.Element => el.attributes.exists {
|
|
721
|
+
case attr: Dom.Attribute.KeyValue if attr.name == "class" => true
|
|
722
|
+
case _ => false
|
|
723
|
+
}
|
|
724
|
+
case _ => false
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Filter by class
|
|
728
|
+
val withClass = page.select(CssSelector.Element("p")).withClass("visible")
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
### Modifying Selections
|
|
732
|
+
|
|
733
|
+
:::warning
|
|
734
|
+
`DomSelection` returns new copies with modifications; the original DOM tree remains unchanged. The `DomSelection` API provides functional transformations. To modify the original DOM tree, use `Dom#transform` with a tree-rewriting function, or rebuild the tree from scratch using the DSL.
|
|
735
|
+
:::
|
|
736
|
+
|
|
737
|
+
The `DomSelection` API provides methods like `.modifyAll`, `.replaceAll`, and `.removeAll` to transform or replace selected nodes:
|
|
738
|
+
|
|
739
|
+
```scala
|
|
740
|
+
import zio.blocks.html._
|
|
741
|
+
|
|
742
|
+
val page = div(
|
|
743
|
+
p("Old 1"),
|
|
744
|
+
p("Old 2"),
|
|
745
|
+
span("Keep")
|
|
746
|
+
)
|
|
747
|
+
|
|
748
|
+
// Transform all Element nodes in the selection; non-Element nodes pass through unchanged
|
|
749
|
+
val modifiedSelection = page.select(CssSelector.Element("p")).modifyAll {
|
|
750
|
+
case el: Dom.Element.Generic if el.tag == "p" => el.copy(tag = "div")
|
|
751
|
+
case other => other
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Replace all selected nodes (returns DomSelection of replacement nodes)
|
|
755
|
+
val replacedSelection = page.select(CssSelector.Element("p")).replaceAll(p("New"))
|
|
756
|
+
|
|
757
|
+
// Remove all selected nodes (returns empty DomSelection)
|
|
758
|
+
val removedSelection = page.select(CssSelector.Element("p")).removeAll
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
:::note
|
|
762
|
+
Pseudo-class selectors (`:hover`, `:focus`, etc.) match elements structurally by their underlying element selector only — they cannot detect browser interaction state in a static DOM tree. `div.hover` matches the same elements as `CssSelector.Element("div")`.
|
|
763
|
+
|
|
764
|
+
Adjacent sibling (`+`) and general sibling (`~`) selectors are supported in CSS output but not in DOM querying — `DomSelection.select` with these combinators returns empty results.
|
|
765
|
+
:::
|
|
766
|
+
|
|
767
|
+
## The CSS ADT
|
|
768
|
+
|
|
769
|
+
The `Css` ADT represents structured stylesheets as a typed data structure, separate from strings.
|
|
770
|
+
|
|
771
|
+
All `Css` subtypes support `.render()` for minified output and `.render(indent: Int)` for indented pretty-printing:
|
|
772
|
+
- `Css.Rule` — single CSS rule with selector and declarations
|
|
773
|
+
- `Css.Sheet` — collection of rules
|
|
774
|
+
- `Css.Raw` — raw CSS string
|
|
775
|
+
- `Css.Comment` — CSS comment
|
|
776
|
+
|
|
777
|
+
### Declarations and Rules
|
|
778
|
+
|
|
779
|
+
A `Css.Declaration` is a property-value pair:
|
|
780
|
+
|
|
781
|
+
```scala
|
|
782
|
+
import zio.blocks.html._
|
|
783
|
+
import zio.blocks.chunk.Chunk
|
|
784
|
+
|
|
785
|
+
val marginDecl = Css.Declaration("margin", "10px")
|
|
786
|
+
val colorDecl = Css.Declaration("color", "blue")
|
|
787
|
+
|
|
788
|
+
val rule = Css.Rule(
|
|
789
|
+
CssSelector.Element("p"),
|
|
790
|
+
Chunk(marginDecl, colorDecl)
|
|
791
|
+
)
|
|
792
|
+
|
|
793
|
+
rule.render
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
### Stylesheets
|
|
797
|
+
|
|
798
|
+
A `Css.Sheet` is a collection of rules:
|
|
799
|
+
|
|
800
|
+
```scala
|
|
801
|
+
import zio.blocks.html._
|
|
802
|
+
import zio.blocks.chunk.Chunk
|
|
803
|
+
|
|
804
|
+
val bodyRule = Css.Rule(
|
|
805
|
+
CssSelector.Element("body"),
|
|
806
|
+
Chunk(
|
|
807
|
+
Css.Declaration("margin", "0"),
|
|
808
|
+
Css.Declaration("font-family", "sans-serif")
|
|
809
|
+
)
|
|
810
|
+
)
|
|
811
|
+
|
|
812
|
+
val stylesheet = Css.Sheet(Chunk(bodyRule))
|
|
813
|
+
|
|
814
|
+
stylesheet.render(indent = 2)
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
### Embedding Stylesheets
|
|
818
|
+
|
|
819
|
+
Embed stylesheets in HTML via the `style()` element:
|
|
820
|
+
|
|
821
|
+
```scala
|
|
822
|
+
import zio.blocks.html._
|
|
823
|
+
import zio.blocks.chunk.Chunk
|
|
824
|
+
|
|
825
|
+
val page = html(
|
|
826
|
+
head(
|
|
827
|
+
style().inlineCss(
|
|
828
|
+
Css.Sheet(Chunk(
|
|
829
|
+
Css.Rule(
|
|
830
|
+
CssSelector.Element("body"),
|
|
831
|
+
Chunk(Css.Declaration("background", "#f0f0f0"))
|
|
832
|
+
)
|
|
833
|
+
))
|
|
834
|
+
)
|
|
835
|
+
),
|
|
836
|
+
body(p("Styled"))
|
|
837
|
+
)
|
|
838
|
+
|
|
839
|
+
page.render
|
|
840
|
+
```
|
|
841
|
+
|
|
842
|
+
### Raw CSS
|
|
843
|
+
|
|
844
|
+
For CSS features not expressible via the ADT (media queries, keyframes, at-rules), use `Css.Raw`:
|
|
845
|
+
|
|
846
|
+
```scala
|
|
847
|
+
import zio.blocks.html._
|
|
848
|
+
|
|
849
|
+
val mediaQuery = Css.Raw("""
|
|
850
|
+
|@media (max-width: 600px) {
|
|
851
|
+
| body { font-size: 14px; }
|
|
852
|
+
|}
|
|
853
|
+
""".stripMargin)
|
|
854
|
+
|
|
855
|
+
println(mediaQuery.render)
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
:::tip
|
|
859
|
+
Prefer `Css.Rule` and `Css.Sheet` over `Css.Raw` when possible — structured CSS enables future optimization and prevents CSS injection. Use `Css.Raw` only for trusted, hardcoded CSS.
|
|
860
|
+
:::
|
|
861
|
+
|
|
862
|
+
### CSS Comments
|
|
863
|
+
|
|
864
|
+
Add comments to stylesheets using `Css.Comment`:
|
|
865
|
+
|
|
866
|
+
```scala
|
|
867
|
+
import zio.blocks.html._
|
|
868
|
+
import zio.blocks.chunk.Chunk
|
|
869
|
+
|
|
870
|
+
val stylesheet = Css.Sheet(Chunk(
|
|
871
|
+
Css.Comment("Mobile-first responsive design"),
|
|
872
|
+
Css.Rule(
|
|
873
|
+
CssSelector.Element("body"),
|
|
874
|
+
Chunk(Css.Declaration("font-size", "16px"))
|
|
875
|
+
),
|
|
876
|
+
Css.Comment("Tablet and desktop breakpoints"),
|
|
877
|
+
Css.Raw("@media (min-width: 768px) { body { font-size: 18px; } }")
|
|
878
|
+
))
|
|
879
|
+
|
|
880
|
+
stylesheet.render(indent = 2)
|
|
881
|
+
```
|
|
882
|
+
|
|
883
|
+
## Rendering
|
|
884
|
+
|
|
885
|
+
All `Dom` and `Css` values support multiple rendering modes.
|
|
886
|
+
|
|
887
|
+
### Minified Rendering
|
|
888
|
+
|
|
889
|
+
`Dom#render` produces compact HTML with no extra whitespace. Use `Dom#renderMinified` as an explicit alias for the same operation:
|
|
890
|
+
|
|
891
|
+
```scala
|
|
892
|
+
import zio.blocks.html._
|
|
893
|
+
|
|
894
|
+
val page = div(h1("Title"), p("Content"))
|
|
895
|
+
page.render
|
|
896
|
+
```
|
|
897
|
+
|
|
898
|
+
### Pretty-Printed Rendering
|
|
899
|
+
|
|
900
|
+
`render(indent: Int)` produces indented, readable output:
|
|
901
|
+
|
|
902
|
+
```scala
|
|
903
|
+
import zio.blocks.html._
|
|
904
|
+
|
|
905
|
+
val page = div(h1("Title"), p("Content"))
|
|
906
|
+
page.render(indent = 2)
|
|
907
|
+
```
|
|
908
|
+
|
|
909
|
+
### Performance Notes
|
|
910
|
+
|
|
911
|
+
- `Dom#render` uses a pre-allocated `StringBuilder` and while-loop rendering — zero allocations for iteration
|
|
912
|
+
- Indentation strings get cached in an array of pre-built space strings (up to 128 characters) to prevent repeated string allocation
|
|
913
|
+
- Void elements automatically self-close with no children
|
|
914
|
+
- Script and Style elements render their children without escaping (with `</` → `<\/` protection for scripts)
|
|
915
|
+
|
|
916
|
+
## Security: XSS Protection
|
|
917
|
+
|
|
918
|
+
The module provides multiple layers of automatic XSS protection:
|
|
919
|
+
|
|
920
|
+
### HTML Text Escaping
|
|
921
|
+
|
|
922
|
+
All `Dom.Text` nodes are HTML-escaped during rendering:
|
|
923
|
+
- `&` → `&`
|
|
924
|
+
- `<` → `<`
|
|
925
|
+
- `>` → `>`
|
|
926
|
+
- `"` → `"`
|
|
927
|
+
- `'` → `'`
|
|
928
|
+
|
|
929
|
+
Untrusted content is always escaped to prevent XSS:
|
|
930
|
+
|
|
931
|
+
```scala
|
|
932
|
+
import zio.blocks.html._
|
|
933
|
+
|
|
934
|
+
val userInput = "<script>alert('XSS')</script>"
|
|
935
|
+
val safe = div(p(userInput))
|
|
936
|
+
|
|
937
|
+
safe.render
|
|
938
|
+
```
|
|
939
|
+
|
|
940
|
+
### JavaScript String Escaping
|
|
941
|
+
|
|
942
|
+
The `ToJs[String]` typeclass escapes strings to prevent breaking out of script contexts:
|
|
943
|
+
- `<` → backslash-u-0-0-3-c (the six-character Unicode escape sequence `<`)
|
|
944
|
+
- `>` → backslash-u-0-0-3-e (the six-character Unicode escape sequence `>`)
|
|
945
|
+
- `&` → backslash-u-0-0-2-6 (the six-character Unicode escape sequence `&`)
|
|
946
|
+
- `"` → `\"`, `'` → `\'`, `\` → `\\`
|
|
947
|
+
- Newlines, carriage returns, and Unicode line/paragraph separators are escaped
|
|
948
|
+
|
|
949
|
+
This protects against `</script>` injection:
|
|
950
|
+
|
|
951
|
+
```scala
|
|
952
|
+
import zio.blocks.html._
|
|
953
|
+
|
|
954
|
+
val userInput = "</script><script>alert('XSS');</script>"
|
|
955
|
+
val code = js"let payload = $userInput"
|
|
956
|
+
|
|
957
|
+
println(code.value)
|
|
958
|
+
// let payload = "</script><script>alert('XSS');</script>"
|
|
959
|
+
// (with < and > characters escaped as Unicode in actual output)
|
|
960
|
+
```
|
|
961
|
+
|
|
962
|
+
### URL Sanitization
|
|
963
|
+
|
|
964
|
+
Attributes named `href`, `src`, `action`, or `formaction` are checked for dangerous schemes at render time:
|
|
965
|
+
- `javascript:`, `vbscript:`, `data:text/html` → prefixed with `unsafe:`
|
|
966
|
+
|
|
967
|
+
Dangerous URLs are automatically sanitized in HTML output:
|
|
968
|
+
|
|
969
|
+
```scala
|
|
970
|
+
import zio.blocks.html._
|
|
971
|
+
|
|
972
|
+
val dangerous = a(href := "javascript:alert('XSS')", "Click me")
|
|
973
|
+
```
|
|
974
|
+
|
|
975
|
+
### No Raw HTML Escape Hatch
|
|
976
|
+
|
|
977
|
+
The module intentionally provides no `Dom.Raw` type for embedding arbitrary HTML. This prevents XSS by ensuring all dynamic content is either constructed via the DSL or interpolated through context-aware interpolators.
|
|
978
|
+
|
|
979
|
+
## Common Patterns
|
|
980
|
+
|
|
981
|
+
The module supports several architectural patterns for code organization and reuse:
|
|
982
|
+
|
|
983
|
+
### Building Reusable Components
|
|
984
|
+
|
|
985
|
+
Define functions that return `Dom.Element` to create reusable components:
|
|
986
|
+
|
|
987
|
+
```scala
|
|
988
|
+
import zio.blocks.html._
|
|
989
|
+
|
|
990
|
+
def card(title: String, content: String): Dom.Element =
|
|
991
|
+
div(
|
|
992
|
+
className := "card",
|
|
993
|
+
h2(title),
|
|
994
|
+
p(content)
|
|
995
|
+
)
|
|
996
|
+
|
|
997
|
+
val page = div(
|
|
998
|
+
card("Card 1", "Content A"),
|
|
999
|
+
card("Card 2", "Content B")
|
|
1000
|
+
)
|
|
1001
|
+
```
|
|
1002
|
+
|
|
1003
|
+
### Conditional Rendering
|
|
1004
|
+
|
|
1005
|
+
Use `when` and `whenSome` for conditional modifiers:
|
|
1006
|
+
|
|
1007
|
+
```scala
|
|
1008
|
+
import zio.blocks.html._
|
|
1009
|
+
|
|
1010
|
+
def userCard(name: String, isAdmin: Boolean): Dom.Element =
|
|
1011
|
+
div(
|
|
1012
|
+
className := "user-card"
|
|
1013
|
+
).when(isAdmin)(
|
|
1014
|
+
className += "admin",
|
|
1015
|
+
span(className := "badge", "Admin")
|
|
1016
|
+
)
|
|
1017
|
+
```
|
|
1018
|
+
|
|
1019
|
+
### Rendering Collections
|
|
1020
|
+
|
|
1021
|
+
Map over collections to create child elements:
|
|
1022
|
+
|
|
1023
|
+
```scala
|
|
1024
|
+
import zio.blocks.html._
|
|
1025
|
+
|
|
1026
|
+
def userList(users: List[String]): Dom.Element =
|
|
1027
|
+
ul(users.map(user => li(user)))
|
|
1028
|
+
|
|
1029
|
+
val page = userList(List("Alice", "Bob", "Charlie"))
|
|
1030
|
+
```
|
|
1031
|
+
|
|
1032
|
+
### Template Composition
|
|
1033
|
+
|
|
1034
|
+
Combine interpolators and DSL for flexibility:
|
|
1035
|
+
|
|
1036
|
+
```scala
|
|
1037
|
+
import zio.blocks.html._
|
|
1038
|
+
|
|
1039
|
+
val title = "My Page"
|
|
1040
|
+
val content = "Welcome to my site"
|
|
1041
|
+
|
|
1042
|
+
val page = html"""
|
|
1043
|
+
<html>
|
|
1044
|
+
<head><title>$title</title></head>
|
|
1045
|
+
<body>
|
|
1046
|
+
${div(p(content))}
|
|
1047
|
+
</body>
|
|
1048
|
+
</html>
|
|
1049
|
+
"""
|
|
1050
|
+
```
|
|
1051
|
+
|
|
1052
|
+
### Querying for Tests
|
|
1053
|
+
|
|
1054
|
+
Use `DomSelection` to write structural assertions:
|
|
1055
|
+
|
|
1056
|
+
```scala
|
|
1057
|
+
import zio.blocks.html._
|
|
1058
|
+
|
|
1059
|
+
val page = div(
|
|
1060
|
+
ul(
|
|
1061
|
+
li(a(href := "/home", "Home")),
|
|
1062
|
+
li(a(href := "/about", "About"))
|
|
1063
|
+
)
|
|
1064
|
+
)
|
|
1065
|
+
|
|
1066
|
+
// Test: check number of links
|
|
1067
|
+
val links = page.select(CssSelector.Element("a"))
|
|
1068
|
+
assert(links.length == 2)
|
|
1069
|
+
|
|
1070
|
+
// Test: check specific href
|
|
1071
|
+
val aboutLink = page.select(a.withAttribute("href", "/about"))
|
|
1072
|
+
assert(aboutLink.texts.contains("About"))
|
|
1073
|
+
```
|
|
1074
|
+
|
|
1075
|
+
## Complete Example: A Dashboard Page
|
|
1076
|
+
|
|
1077
|
+
Here is a complete, self-contained example building a full dashboard page with navigation, metadata, styling, and content sections:
|
|
1078
|
+
|
|
1079
|
+
```scala
|
|
1080
|
+
import zio.blocks.html._
|
|
1081
|
+
import zio.blocks.chunk.Chunk
|
|
1082
|
+
|
|
1083
|
+
val userName = "Alice"
|
|
1084
|
+
val items = List("Dashboard", "Settings", "Logout")
|
|
1085
|
+
|
|
1086
|
+
val pieces: Chunk[Dom] = Chunk(
|
|
1087
|
+
doctype,
|
|
1088
|
+
html(
|
|
1089
|
+
head(
|
|
1090
|
+
meta(charset := "utf-8"),
|
|
1091
|
+
meta(name := "viewport", content := "width=device-width, initial-scale=1.0"),
|
|
1092
|
+
title("My App"),
|
|
1093
|
+
link(rel := "stylesheet", href := "/style.css"),
|
|
1094
|
+
style().inlineCss(
|
|
1095
|
+
Css.Sheet(Chunk(
|
|
1096
|
+
Css.Rule(CssSelector.Element("body"), Chunk(
|
|
1097
|
+
Css.Declaration("margin", "0"),
|
|
1098
|
+
Css.Declaration("font-family", "sans-serif")
|
|
1099
|
+
))
|
|
1100
|
+
))
|
|
1101
|
+
)
|
|
1102
|
+
),
|
|
1103
|
+
body(
|
|
1104
|
+
header(
|
|
1105
|
+
nav(items.map(item => a(href := "#", item)))
|
|
1106
|
+
),
|
|
1107
|
+
main(
|
|
1108
|
+
h1(s"Welcome, $userName!"),
|
|
1109
|
+
p("This is your dashboard.")
|
|
1110
|
+
),
|
|
1111
|
+
footer(
|
|
1112
|
+
p("© 2026 My App")
|
|
1113
|
+
),
|
|
1114
|
+
script().inlineJs(js"console.log('Page loaded');")
|
|
1115
|
+
)
|
|
1116
|
+
)
|
|
1117
|
+
)
|
|
1118
|
+
|
|
1119
|
+
pieces.map(_.render(indent = 2)).mkString("\n")
|
|
1120
|
+
```
|