joys 0.1.0 → 0.1.2
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.
- checksums.yaml +4 -4
- data/.DS_Store +0 -0
- data/CHANGELOG.md +9 -0
- data/README.md +1012 -3
- data/joys-0.1.0.gem +0 -0
- data/joys-0.1.1.gem +0 -0
- data/lib/joys/core.rb +161 -0
- data/lib/joys/helpers.rb +111 -0
- data/lib/joys/styles.rb +156 -0
- data/lib/joys/tags.rb +120 -0
- data/lib/joys/version.rb +1 -1
- data/lib/joys.rb +4 -5
- metadata +7 -1
data/README.md
CHANGED
@@ -1,5 +1,1014 @@
|
|
1
|
-
# Joys
|
1
|
+
# Joys - HTML Templates at the Speed of Light
|
2
2
|
|
3
|
-
|
3
|
+
**Write HTML like Ruby, render it at the speed of C.**
|
4
4
|
|
5
|
-
|
5
|
+
Joys is a pure-Ruby HTML templating engine with built-in CSS and ludicrous-speed rendering.
|
6
|
+
|
7
|
+
## Why Joys?
|
8
|
+
|
9
|
+
- **Blazing Fast** - 3x faster on cold render. Orders of magnitude faster after caching.
|
10
|
+
- **Pure Ruby** - No new syntax to learn. It's just methods and blocks.
|
11
|
+
- **Testable** - Components are just functions. Test them like any Ruby code.
|
12
|
+
- **Fully Integrated Styling** - Deduplicated, sorted css right inside your components
|
13
|
+
- **Framework Agnostic** - Use it in Rails, Roda, Sinatra, Hanami, Syro, etc.
|
14
|
+
|
15
|
+
---
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
gem 'joys'
|
21
|
+
```
|
22
|
+
|
23
|
+
## Quick Start: The Power of Joys DSL
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
html = Joys.html do
|
27
|
+
div(cs: "card") do
|
28
|
+
h1 "Hello"
|
29
|
+
p "World", cs: "text-sm"
|
30
|
+
txt "It's me!"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
puts html
|
35
|
+
|
36
|
+
# => <div class="card"><h1>Hello</h1><p class="text-sm">World</p>It's me!</div>
|
37
|
+
```
|
38
|
+
|
39
|
+
### Security & HTML Escaping
|
40
|
+
|
41
|
+
Joys ships with sensible defaults to prevent XSS attacks.
|
42
|
+
|
43
|
+
All text nodes must be rendered explicitly via the `txt()` or `raw()` helpers
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
# txt escapes HTML (safe by default)
|
47
|
+
p { txt user_input } # "<script>alert('xss')</script>" becomes safe text
|
48
|
+
|
49
|
+
# raw outputs HTML (use with caution)
|
50
|
+
div { raw markdown_to_html(content) }
|
51
|
+
|
52
|
+
# Pass raw: true to any tag
|
53
|
+
div(content, raw: true) # content renders as HTML
|
54
|
+
```
|
55
|
+
Following Ruby conventions, adding a bang `!` to the elemtn indicates a method with a more 'potent' or 'dangerous' effect—in this case, disabling HTML escaping.
|
56
|
+
|
57
|
+
```
|
58
|
+
# Escaped HTML
|
59
|
+
div("hello<br>world") # <div>Hello<br>world</div>
|
60
|
+
|
61
|
+
# Raw Unescaped HTML with bang (div!)
|
62
|
+
div!("hello<br>world") # <div>Hello<br>world</div>
|
63
|
+
```
|
64
|
+
|
65
|
+
### Smart Class Handling
|
66
|
+
|
67
|
+
Of course, you can use `class` attribute, but Joys provides `cs` for brevity given how often it is used.
|
68
|
+
|
69
|
+
You also get some extra benefits from `cs`:
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
# String, Array, or conditional classes - all just work
|
73
|
+
div(cs: "card")
|
74
|
+
div(cs: ["card", "active"])
|
75
|
+
div(cs: ["card", user.premium? && "premium"])
|
76
|
+
div(cs: ["base", ("active" if active?)])
|
77
|
+
```
|
78
|
+
|
79
|
+
### Boolean Attributes That Don't Suck
|
80
|
+
|
81
|
+
Elegant handling with underscore-to-dash conversion and proper boolean semantics:
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
input(type: "text", auto_focus: true, form_novalidate: true)
|
85
|
+
# => <input type="text" autofocus formnovalidate>
|
86
|
+
|
87
|
+
input(type: "text", disabled: false)
|
88
|
+
# => <input type="text">
|
89
|
+
```
|
90
|
+
|
91
|
+
Write attributes naturally with underscores - Joys outputs the correct HTML boolean names. Works automatically for all boolean attributes, including: `autofocus`, `autoplay`, `checked`, `controls`, `disabled`, `default`, `formnovalidate`, `hidden`, `inert`, `itemscope`, `loop`, `multiple`, `muted`, `novalidate`, `open`, `readonly`, `required`, `reversed`, `scoped`, `seamless` and `selected`.
|
92
|
+
|
93
|
+
### Smart Data and ARIA Attributes
|
94
|
+
|
95
|
+
Nested hashes auto-expand with underscore-to-dash conversion and intelligent false handling:
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
button(
|
99
|
+
"Save",
|
100
|
+
cs: "btn",
|
101
|
+
data: {
|
102
|
+
controller: "form_handler",
|
103
|
+
user_id: 123,
|
104
|
+
confirm_message: "Are you sure?"
|
105
|
+
},
|
106
|
+
aria: {
|
107
|
+
label: "Save changes",
|
108
|
+
expanded: false, # Boolean false - omitted
|
109
|
+
disabled: "false" # String false - rendered
|
110
|
+
}
|
111
|
+
)
|
112
|
+
# => <button class="btn" data-controller="form_handler" data-user-id="123"
|
113
|
+
# data-confirm-message="Are you sure?" aria-label="Save changes"
|
114
|
+
# aria-disabled="false">Save</button>
|
115
|
+
```
|
116
|
+
|
117
|
+
**Key behaviors:**
|
118
|
+
- Underscores convert to dashes: `user_id` → `data-user-id`
|
119
|
+
- Boolean `false` values get omitted entirely
|
120
|
+
- String `"false"` values render normally
|
121
|
+
- All other values render with proper escaping
|
122
|
+
|
123
|
+
### Every HTML Tag, Zero Boilerplate
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
# All standard HTML tags just work
|
127
|
+
article {
|
128
|
+
header { h1("Post Title") }
|
129
|
+
section { p("Content...") }
|
130
|
+
footer { time(datetime: "2024-01-01") { txt "Jan 1" } }
|
131
|
+
}
|
132
|
+
|
133
|
+
# Boolean attributes are smart
|
134
|
+
input(type: "checkbox", checked: user.subscribed?)
|
135
|
+
# => <input type="checkbox" checked> (if subscribed)
|
136
|
+
# => <input type="checkbox"> (if not)
|
137
|
+
|
138
|
+
# Void tags handled correctly
|
139
|
+
img(src: "/logo.png", alt: "Logo")
|
140
|
+
input(type: "text", name: "email", required: true)
|
141
|
+
```
|
142
|
+
|
143
|
+
---
|
144
|
+
|
145
|
+
## Building Components
|
146
|
+
|
147
|
+
Components are reusable pieces of UI. Define once, use everywhere:
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
Joys.define(:comp, :user_card) do |user|
|
151
|
+
div(cs: "card") {
|
152
|
+
img(src: user[:avatar], alt: user[:name])
|
153
|
+
h3(user[:name])
|
154
|
+
p { txt user[:bio] } # txt escapes HTML
|
155
|
+
}
|
156
|
+
end
|
157
|
+
|
158
|
+
# Render standalone
|
159
|
+
html = Joys.comp(:user_card, {name: "Alice", bio: "Developer"})
|
160
|
+
|
161
|
+
# Or use inside other components
|
162
|
+
Joys.define(:comp, :user_list) do |users|
|
163
|
+
div(cs: "grid") {
|
164
|
+
users.each { |user| comp(:user_card, user) }
|
165
|
+
}
|
166
|
+
end
|
167
|
+
```
|
168
|
+
|
169
|
+
#### A Note on Naming Conventions
|
170
|
+
|
171
|
+
**Joys** uses a simple global registry for components, pages, and layouts.
|
172
|
+
|
173
|
+
For larger applications, we recommend namespacing your definitions to avoid collisions.
|
174
|
+
|
175
|
+
Symbols and strings both work, so you can adopt a convention that fits your project.
|
176
|
+
|
177
|
+
```ruby
|
178
|
+
# Define with a path-like string
|
179
|
+
Joys.define(:comp, 'forms/button') do |text|
|
180
|
+
# ...
|
181
|
+
end
|
182
|
+
Joys.define(:comp, 'cards/user_profile') do |user|
|
183
|
+
# ...
|
184
|
+
end
|
185
|
+
|
186
|
+
# Use them elsewhere
|
187
|
+
|
188
|
+
Joys.html do
|
189
|
+
div {
|
190
|
+
comp 'cards/user_profile', @user
|
191
|
+
comp 'forms/button', "Submit"
|
192
|
+
}
|
193
|
+
end
|
194
|
+
```
|
195
|
+
|
196
|
+
This approach keeps your component library organized as it grows.
|
197
|
+
|
198
|
+
### Pages
|
199
|
+
|
200
|
+
Pages are top-level templates, typically rendered by controllers. Unlike other template solutions, Joys expects each page to explicitly define its layout.
|
201
|
+
|
202
|
+
```ruby
|
203
|
+
Joys.define(:page, :dashboard) do
|
204
|
+
layout(:main) {
|
205
|
+
push(:title) { txt "Dashboard" }
|
206
|
+
|
207
|
+
push(:main) {
|
208
|
+
h1("Welcome back!")
|
209
|
+
comp(:stats_panel, @stats)
|
210
|
+
|
211
|
+
div(cs: "users-grid") {
|
212
|
+
@users.each { |u| comp(:user_card, u) }
|
213
|
+
}
|
214
|
+
}
|
215
|
+
}
|
216
|
+
end
|
217
|
+
|
218
|
+
# Render with locals
|
219
|
+
html = Joys.page(:dashboard, users: User.all, stats: calc_stats)
|
220
|
+
```
|
221
|
+
|
222
|
+
Similar to `content_for` in Rails, HTML may be sent to the layout via the `push(:content_name) {...}` method. The layout establishes placement of this content using `pull(:content_name)`
|
223
|
+
|
224
|
+
Note: There is no default `yield`. The `pull` method always requires a symbol. It's up to the user to be explicit in defining where to push content. `push(:main)` or `push(:body)` are sensible defualts, but feel free to use whatever you want.
|
225
|
+
|
226
|
+
### Layouts
|
227
|
+
|
228
|
+
Layouts provide consistent structure with content slots via the `pull()` method:
|
229
|
+
|
230
|
+
```ruby
|
231
|
+
Joys.define(:layout, :main) do
|
232
|
+
html(lang: "en") {
|
233
|
+
head {
|
234
|
+
title { pull(:title) }
|
235
|
+
link(href: "/app.css", rel: "stylesheet")
|
236
|
+
}
|
237
|
+
body {
|
238
|
+
nav { comp(:navbar) }
|
239
|
+
main(cs: "container") {
|
240
|
+
pull(:main)
|
241
|
+
}
|
242
|
+
}
|
243
|
+
}
|
244
|
+
end
|
245
|
+
```
|
246
|
+
|
247
|
+
---
|
248
|
+
|
249
|
+
## Rails Integration
|
250
|
+
|
251
|
+
### Setup
|
252
|
+
|
253
|
+
```ruby
|
254
|
+
# config/initializers/joys.rb
|
255
|
+
require 'joys/rails'
|
256
|
+
```
|
257
|
+
|
258
|
+
### File Structure
|
259
|
+
|
260
|
+
```
|
261
|
+
app/views/joys/
|
262
|
+
├── layouts/
|
263
|
+
│ └── application.rb
|
264
|
+
├── comps/
|
265
|
+
│ ├── navbar.rb
|
266
|
+
│ └── user_card.rb
|
267
|
+
└── pages/
|
268
|
+
├── dashboard.rb
|
269
|
+
└── users_show.rb
|
270
|
+
```
|
271
|
+
|
272
|
+
### Rails Helpers Work Seamlessly
|
273
|
+
|
274
|
+
Joys is fully compatible with Rails `ActiveSupport::SafeBuffer`, so you can use all Rails helpers seamlessly.
|
275
|
+
|
276
|
+
```ruby
|
277
|
+
Joys.define(:page, :users_edit) do
|
278
|
+
layout(:main) {
|
279
|
+
push(:main) {
|
280
|
+
# Rails form helpers just work
|
281
|
+
form_with(model: @user) do |f|
|
282
|
+
div(cs: "field") {
|
283
|
+
f.label(:name)
|
284
|
+
f.text_field(:name, class: "input")
|
285
|
+
}
|
286
|
+
f.submit("Save", class: "btn btn-primary")
|
287
|
+
end
|
288
|
+
# As do route helpers
|
289
|
+
a("Back", href: users_path, cs: "link")
|
290
|
+
}
|
291
|
+
}
|
292
|
+
end
|
293
|
+
```
|
294
|
+
|
295
|
+
Rendering from controllers could not be more simple!
|
296
|
+
|
297
|
+
```
|
298
|
+
class UsersController < ApplicationController
|
299
|
+
def show
|
300
|
+
@user = User.find(params[:id])
|
301
|
+
render_joys :users_show # renders from #{Rails.root}/app/views/joys/users_show.rb
|
302
|
+
end
|
303
|
+
end
|
304
|
+
```
|
305
|
+
|
306
|
+
---
|
307
|
+
|
308
|
+
## Real-World Example
|
309
|
+
|
310
|
+
Here's a more complex nested component with conditions, iterations and all the usual good stuff we expect from templates.
|
311
|
+
|
312
|
+
Since Joys is just ruby you have unlimited power to present the content as you wish
|
313
|
+
|
314
|
+
```ruby
|
315
|
+
Joys.define(:comp, :pricing_card) do |plan|
|
316
|
+
div(cs: ["card", plan[:featured] && "featured"]) {
|
317
|
+
if plan[:featured]
|
318
|
+
div("Most Popular", cs: "badge")
|
319
|
+
end
|
320
|
+
|
321
|
+
h3(plan[:name])
|
322
|
+
|
323
|
+
div(cs: "price") {
|
324
|
+
span("$", cs: "currency")
|
325
|
+
span(plan[:price].to_s, cs: "amount")
|
326
|
+
span("/mo", cs: "period")
|
327
|
+
}
|
328
|
+
|
329
|
+
ul(cs: "features") {
|
330
|
+
plan[:features].each do |feature|
|
331
|
+
li { raw "✓ #{feature}"}
|
332
|
+
end
|
333
|
+
}
|
334
|
+
|
335
|
+
button(
|
336
|
+
plan[:featured] ? "Start Free Trial" : "Choose Plan",
|
337
|
+
cs: ["btn", "w-full", (plan[:featured] ? "btn-primary" : "btn-secondary")],
|
338
|
+
data: {
|
339
|
+
plan: plan[:id],
|
340
|
+
price: plan[:price]
|
341
|
+
}
|
342
|
+
)
|
343
|
+
}
|
344
|
+
end
|
345
|
+
```
|
346
|
+
---
|
347
|
+
## A Word on Performance
|
348
|
+
|
349
|
+
Using simple components and views (only 3 layers of nesting) we can see how Joy stacks up:
|
350
|
+
|
351
|
+
### Simple Templates (385 chars)
|
352
|
+
|
353
|
+
#### ❄️ COLD RENDER (new object per call)
|
354
|
+
| | user | system | total | real. |
|
355
|
+
|------|--------|--------|--------|--------|
|
356
|
+
|Joys: |0.060535|0.000200|0.060735|0.060736|
|
357
|
+
|Slim: |0.048344|0.000183|0.048527|0.048530|
|
358
|
+
|ERB: |0.301811|0.000808|0.302619|0.302625|
|
359
|
+
|Phlex:|0.069636|0.000470|0.070106|0.071157|
|
360
|
+
|
361
|
+
#### 🔥 HOT RENDER (cached objects)
|
362
|
+
| | user | system | total | real |
|
363
|
+
|------|--------|---------|--------|--------|
|
364
|
+
|Joys: |0.065255| 0.000257|0.065512|0.065512|
|
365
|
+
|Slim: |0.049323| 0.000295|0.049618|0.049695|
|
366
|
+
|ERB: |0.309757| 0.001167|0.310924|0.310929|
|
367
|
+
|Phlex:|0.069663| 0.000141|0.069804|0.069805|
|
368
|
+
|
369
|
+
#### 💾 MEMORY USAGE
|
370
|
+
Joys memory: 532356 bytes
|
371
|
+
Slim memory: 40503436 bytes
|
372
|
+
Phlex memory: 8000 bytes
|
373
|
+
ERB memory: 1669256 bytes
|
374
|
+
|
375
|
+
At smaller scales performance is on par with Phlex, which has excellent speed and superior
|
376
|
+
|
377
|
+
### Complex Templates (8,000+ chars)
|
378
|
+
|
379
|
+
As template complexity grows, Joys starts to really show off its optimizations. This benchmark tests a full dashboard page with 5 components and 2 loops yields:
|
380
|
+
|
381
|
+
* Data parsing
|
382
|
+
* Layouts
|
383
|
+
* Multiple Content Slots
|
384
|
+
* Multiple Components
|
385
|
+
* Several Iterations
|
386
|
+
* Conditions
|
387
|
+
|
388
|
+
### ❄️ COLD RENDER (new object per call)
|
389
|
+
|
390
|
+
This is the bare bones benchmark, no "cheating" allowed.
|
391
|
+
|
392
|
+
| | user | system | total | real |
|
393
|
+
|------|--------|--------|--------|--------|
|
394
|
+
| Joys |0.051715|0.000364|0.052079|0.052080|
|
395
|
+
| ERB |0.520495|0.003696|0.524191|0.524187|
|
396
|
+
| Slim |6.001650|0.019418|6.021068|6.021013|
|
397
|
+
| Phlex|0.169567|0.000373|0.169940|0.169938|
|
398
|
+
|
399
|
+
Note: Joys achieves its 'cold render' speed by eliminating object overhead and using simple string operations with smart memory management.
|
400
|
+
|
401
|
+
### 🔥 HOT RENDER (cached objects)
|
402
|
+
|
403
|
+
| | user | system | total | real |
|
404
|
+
|------|---------|--------|---------|--------|
|
405
|
+
|JOYS: | 0.000463|0.000010| 0.000473|0.000473|
|
406
|
+
|SLIM: | 0.045881|0.000358| 0.046239|0.046243|
|
407
|
+
|PHLEX:| 0.167631|0.000760| 0.168391|0.168661|
|
408
|
+
|ERB: | 0.394509|0.004084| 0.398593|0.398614|
|
409
|
+
|
410
|
+
Note: Joys achieves its 'hot render' speed by compiling a template's structure into a highly optimized render function the first time it's called. Subsequent calls with the same component structure reuse this function, bypassing compilation and object allocation, and only interpolating the dynamic data.
|
411
|
+
|
412
|
+
|
413
|
+
### 💾 MEMORY USAGE
|
414
|
+
|
415
|
+
Joys memory: 7587400 bytes
|
416
|
+
Slim memory: 217778600 bytes
|
417
|
+
Phlex memory: 9956000 bytes
|
418
|
+
ERB memory: 7264240 bytes
|
419
|
+
|
420
|
+
Even without caching, Joys is rendering at around 50 milliseconds.
|
421
|
+
|
422
|
+
The real performance comes after caching at half a millisecond.
|
423
|
+
|
424
|
+
That's not just fast, its _ludicrous speed_ 🚀 All thanks to Joys's one-time compilation and caching of the template structure.
|
425
|
+
|
426
|
+
But don't take our word for it:
|
427
|
+
|
428
|
+
```
|
429
|
+
gem install erb slim phlex joys
|
430
|
+
|
431
|
+
# Simple benchmark
|
432
|
+
ruby text/bench/simple.rb
|
433
|
+
|
434
|
+
# Complex benchmark
|
435
|
+
ruby text/bench/deep.rb
|
436
|
+
```
|
437
|
+
|
438
|
+
Note: If you have any additions, critiques or input on these benchmarks please submit a pull request.
|
439
|
+
|
440
|
+
### Performance Philosophy
|
441
|
+
|
442
|
+
Joys attains its incredible speed through:
|
443
|
+
|
444
|
+
1. **Precompiled layouts** - Templates compile once, render many
|
445
|
+
2. **Direct string building** - No intermediate AST or object allocation
|
446
|
+
3. **Smart caching** - Components cache by arguments automatically
|
447
|
+
4. **Zero abstraction penalty** - It's just method calls and string concatenation
|
448
|
+
|
449
|
+
All achieved without C or Rust. Just plain old Ruby, with a core of under 500 lines of code.
|
450
|
+
|
451
|
+
---
|
452
|
+
|
453
|
+
|
454
|
+
## Joy Styling – Scoped, Deduped, Responsive CSS Without the Hassle
|
455
|
+
|
456
|
+
Joys doesn’t just generate HTML—it handles CSS like a **pure Ruby, React-level, zero-Node, blazing-fast styling engine**.
|
457
|
+
|
458
|
+
With **`styles`**, you can write component CSS **inline**, **scoped**, **deduped**, and **responsive**—all automatically, without ever touching a separate asset pipeline.
|
459
|
+
|
460
|
+
---
|
461
|
+
|
462
|
+
### Basic Usage
|
463
|
+
|
464
|
+
```ruby
|
465
|
+
|
466
|
+
# Make sure your layout has a style tag with pull_styles
|
467
|
+
|
468
|
+
Joys.define(:layout, :main) do
|
469
|
+
doctype
|
470
|
+
html {
|
471
|
+
head {
|
472
|
+
style { pull_styles } # all css will be rendered here
|
473
|
+
}
|
474
|
+
}
|
475
|
+
end
|
476
|
+
|
477
|
+
# Works when registering coomponents and pages
|
478
|
+
|
479
|
+
Joys.define(:comp, :card) do
|
480
|
+
div(cs: "card") do
|
481
|
+
h1("Hello", cs: "head")
|
482
|
+
p("Welcome to Joys!")
|
483
|
+
end
|
484
|
+
|
485
|
+
styles do
|
486
|
+
css ".card p { color:#333; font-size:0.875rem }"
|
487
|
+
end
|
488
|
+
end
|
489
|
+
|
490
|
+
html = Joys.comp(:card)
|
491
|
+
puts html
|
492
|
+
```
|
493
|
+
|
494
|
+
Note: For multiline css statements you may use Ruby's `%()` method as follows
|
495
|
+
|
496
|
+
```ruby
|
497
|
+
styles do
|
498
|
+
css %w(
|
499
|
+
.card p { color:#333; font-size:0.875rem }
|
500
|
+
.card { background:#fff; border-radius:6px; padding:1rem }
|
501
|
+
)
|
502
|
+
end
|
503
|
+
```
|
504
|
+
|
505
|
+
**Output HTML:**
|
506
|
+
|
507
|
+
```html
|
508
|
+
<div class="card">
|
509
|
+
<h1 class="head">Hello</h1>
|
510
|
+
<p>Welcome to Joys!</p>
|
511
|
+
</div>
|
512
|
+
```
|
513
|
+
|
514
|
+
**Generated CSS (automatically appended):**
|
515
|
+
|
516
|
+
```css
|
517
|
+
.card { background:#fff; border-radius:6px; padding:1rem }
|
518
|
+
.card p { color:#333; font-size:0.875rem }
|
519
|
+
```
|
520
|
+
|
521
|
+
---
|
522
|
+
|
523
|
+
### Automatic CSS Scoping
|
524
|
+
|
525
|
+
Want your component styles **prefixed automatically** to avoid collisions? Just opt in with `scoped: true`:
|
526
|
+
|
527
|
+
```ruby
|
528
|
+
Joys.define(:comp, :user) do
|
529
|
+
styles(scoped: true) do
|
530
|
+
css %w(
|
531
|
+
.card { background:#fff }
|
532
|
+
.head { font-weight:bold }
|
533
|
+
)
|
534
|
+
end
|
535
|
+
end
|
536
|
+
```
|
537
|
+
|
538
|
+
**Generated CSS (prefixed with component name):**
|
539
|
+
|
540
|
+
```css
|
541
|
+
.user .card { background:#fff }
|
542
|
+
.user .head { font-weight:bold }
|
543
|
+
```
|
544
|
+
|
545
|
+
> No more worrying about collisions with other components, layouts, or third-party CSS.
|
546
|
+
|
547
|
+
---
|
548
|
+
|
549
|
+
### Responsive Media Queries
|
550
|
+
|
551
|
+
Joys supports **`min_media`**, **`max_media`**, and **`minmax_media`** right inside your styles. Just pass the width and CSS string.
|
552
|
+
|
553
|
+
```ruby
|
554
|
+
styles do
|
555
|
+
css %w(
|
556
|
+
.card { padding:1rem }
|
557
|
+
)
|
558
|
+
|
559
|
+
media_min "768px", %(
|
560
|
+
.card { padding:2rem }
|
561
|
+
.head { padding:3rem }
|
562
|
+
)
|
563
|
+
|
564
|
+
media_max "480px", ".card { padding:0.5rem }"
|
565
|
+
media_minmax "481px", "767px", ".card { padding:1rem }"
|
566
|
+
end
|
567
|
+
```
|
568
|
+
|
569
|
+
**Generated CSS:**
|
570
|
+
|
571
|
+
```css
|
572
|
+
.card { padding:1rem }
|
573
|
+
|
574
|
+
@media (min-width: 768px) {
|
575
|
+
.card { padding:2rem }
|
576
|
+
.head { padding:3rem }
|
577
|
+
}
|
578
|
+
|
579
|
+
@media (max-width: 480px) {
|
580
|
+
.card { padding:0.5rem }
|
581
|
+
}
|
582
|
+
|
583
|
+
@media (min-width: 481px) and (max-width: 767px) {
|
584
|
+
.card { padding:1rem }
|
585
|
+
}
|
586
|
+
```
|
587
|
+
|
588
|
+
* ✅ **Merged automatically**
|
589
|
+
* ✅ **Deduped**
|
590
|
+
* ✅ **Sorted by breakpoint**
|
591
|
+
* ✅ **Scoped if opted-in**
|
592
|
+
|
593
|
+
And yes, we also have container queries!
|
594
|
+
|
595
|
+
```ruby
|
596
|
+
styles do
|
597
|
+
container_min "768px", ".card {font-size:1.2em}"
|
598
|
+
named_container_min "sidebar", "768px", ".card {font-size:2em}"
|
599
|
+
end
|
600
|
+
```
|
601
|
+
|
602
|
+
**Generated CSS:**
|
603
|
+
|
604
|
+
```css
|
605
|
+
@container (min-width:768px) {
|
606
|
+
.card {font-size:1.2em}
|
607
|
+
}
|
608
|
+
@container sidebar (min-width:768px) {
|
609
|
+
.card {font-size:1.2em}
|
610
|
+
}
|
611
|
+
```
|
612
|
+
|
613
|
+
---
|
614
|
+
|
615
|
+
### Deduplication & Ordering – Built In
|
616
|
+
|
617
|
+
Write your component styles wherever you like. Joys will:
|
618
|
+
|
619
|
+
* Remove **duplicate selectors** automatically
|
620
|
+
* Preserve **correct CSS order across nested components**
|
621
|
+
* Merge **global, page-level, and component-level styles** seamlessly
|
622
|
+
|
623
|
+
```ruby
|
624
|
+
# Even if .card appears in multiple components:
|
625
|
+
styles do
|
626
|
+
css ".card { margin:0 }"
|
627
|
+
end
|
628
|
+
|
629
|
+
# Another component:
|
630
|
+
styles do
|
631
|
+
css ".card { margin:0 }" # Joys dedupes automatically
|
632
|
+
end
|
633
|
+
```
|
634
|
+
|
635
|
+
---
|
636
|
+
|
637
|
+
### Rendering External CSS
|
638
|
+
|
639
|
+
For teams that prefer external CSS files over inline styling, Joys can automatically generate and serve CSS files while maintaining all the performance benefits of consolidated, deduped styles.
|
640
|
+
|
641
|
+
#### Basic Usage
|
642
|
+
|
643
|
+
Replace `pull_styles` with `pull_external_styles` in your layout:
|
644
|
+
|
645
|
+
```ruby
|
646
|
+
Joys.define(:layout, :main) do
|
647
|
+
html(lang: "en") {
|
648
|
+
head {
|
649
|
+
title { pull(:title) }
|
650
|
+
pull_external_styles # Generates <link> tags instead of inline styles
|
651
|
+
}
|
652
|
+
body {
|
653
|
+
pull(:main)
|
654
|
+
}
|
655
|
+
}
|
656
|
+
end
|
657
|
+
```
|
658
|
+
|
659
|
+
#### How It Works
|
660
|
+
|
661
|
+
When you render a page, Joys:
|
662
|
+
|
663
|
+
1. **Analyzes** which components are actually used
|
664
|
+
2. **Consolidates** all their styles (base CSS + media queries + container queries)
|
665
|
+
3. **Generates** a CSS file named after the component combination
|
666
|
+
4. **Caches** the file to disk (only creates once per unique component set)
|
667
|
+
5. **Returns** the appropriate `<link>` tag
|
668
|
+
|
669
|
+
```ruby
|
670
|
+
# Page uses: navbar, user_card, button components
|
671
|
+
# Generates: /css/comp_navbar,comp_user_card,comp_button.css
|
672
|
+
# Returns: <link rel="stylesheet" href="/css/comp_navbar,comp_user_card,comp_button.css">
|
673
|
+
```
|
674
|
+
|
675
|
+
#### Configuration
|
676
|
+
|
677
|
+
Set your CSS output directory (defaults to `"public/css"`):
|
678
|
+
|
679
|
+
```ruby
|
680
|
+
# config/initializers/joys.rb (Rails)
|
681
|
+
Joys.css_path = Rails.root.join("public", "assets", "css")
|
682
|
+
|
683
|
+
# Or for other frameworks
|
684
|
+
Joys.css_path = "public/stylesheets"
|
685
|
+
```
|
686
|
+
|
687
|
+
#### Automatic File Management
|
688
|
+
|
689
|
+
Joys handles the file lifecycle automatically:
|
690
|
+
|
691
|
+
- **Smart naming** - Files named by component combination ensure perfect caching
|
692
|
+
- **No duplication** - Same component set = same file, served from cache
|
693
|
+
- **Directory creation** - Creates nested directories as needed
|
694
|
+
- **Production ready** - Files persist across deployments
|
695
|
+
|
696
|
+
#### Example Generated CSS
|
697
|
+
|
698
|
+
```ruby
|
699
|
+
Joys.define(:comp, :card) do
|
700
|
+
div(cs: "card") { yield }
|
701
|
+
|
702
|
+
styles do
|
703
|
+
css ".card { padding: 1rem; background: white; }"
|
704
|
+
media_min(768, ".card { padding: 2rem; }")
|
705
|
+
end
|
706
|
+
end
|
707
|
+
|
708
|
+
Joys.define(:comp, :button) do
|
709
|
+
button(cs: "btn") { yield }
|
710
|
+
|
711
|
+
styles(scoped: true) do
|
712
|
+
css ".btn { padding: 0.5rem 1rem; }"
|
713
|
+
container_min(300, ".btn { width: 100%; }")
|
714
|
+
end
|
715
|
+
end
|
716
|
+
```
|
717
|
+
|
718
|
+
**Generated CSS file:**
|
719
|
+
```css
|
720
|
+
.card { padding: 1rem; background: white; }
|
721
|
+
.button .btn { padding: 0.5rem 1rem; }
|
722
|
+
@media (min-width: 768px){
|
723
|
+
.card { padding: 2rem; }
|
724
|
+
}
|
725
|
+
@container (min-width: 300px){
|
726
|
+
.button .btn { width: 100%; }
|
727
|
+
}
|
728
|
+
```
|
729
|
+
|
730
|
+
#### Performance Characteristics
|
731
|
+
|
732
|
+
External CSS provides:
|
733
|
+
|
734
|
+
- **Browser caching** - CSS files cached separately from HTML
|
735
|
+
- **Parallel loading** - CSS downloads while HTML processes
|
736
|
+
- **CDN friendly** - Static files easily cached at edge locations
|
737
|
+
- **Same deduplication** - All Joys optimizations still apply
|
738
|
+
|
739
|
+
Trade-offs vs inline:
|
740
|
+
|
741
|
+
- **Extra HTTP request** - One additional round trip
|
742
|
+
- **Render blocking** - CSS must load before styled rendering
|
743
|
+
- **Cache complexity** - More moving parts in deployment
|
744
|
+
|
745
|
+
#### When to Use External CSS
|
746
|
+
|
747
|
+
Choose external CSS when you have:
|
748
|
+
|
749
|
+
- **Large stylesheets** (>20KB) where caching outweighs request overhead
|
750
|
+
- **Strict CSP policies** that prohibit inline styles
|
751
|
+
- **Team preferences** for traditional CSS file organization
|
752
|
+
- **CDN optimization** requirements
|
753
|
+
|
754
|
+
For most applications under 30-40KB of CSS, inline styles offer better performance and simpler deployment.
|
755
|
+
|
756
|
+
#### Rails Integration
|
757
|
+
|
758
|
+
External CSS works seamlessly with Rails asset pipeline:
|
759
|
+
|
760
|
+
```ruby
|
761
|
+
# config/initializers/joys.rb
|
762
|
+
Joys.css_path = Rails.root.join("public", "assets", "joys")
|
763
|
+
|
764
|
+
# Your layout
|
765
|
+
Joys.define(:layout, :application) do
|
766
|
+
html {
|
767
|
+
head {
|
768
|
+
stylesheet_link_tag "application", "data-turbo-track": "reload"
|
769
|
+
pull_external_styles # Joys-generated styles
|
770
|
+
}
|
771
|
+
}
|
772
|
+
end
|
773
|
+
```
|
774
|
+
|
775
|
+
#### Deployment Considerations
|
776
|
+
|
777
|
+
Since CSS files are generated at runtime:
|
778
|
+
|
779
|
+
- Ensure write permissions on your CSS directory
|
780
|
+
- Consider warming the cache on deployment
|
781
|
+
- Add `*.css` to your `.gitignore` if files are in your repo path
|
782
|
+
- For containerized deployments, mount a persistent volume for the CSS directory
|
783
|
+
|
784
|
+
#### Mixing with Traditional Assets
|
785
|
+
|
786
|
+
External Joys CSS plays nicely with existing stylesheets:
|
787
|
+
|
788
|
+
```ruby
|
789
|
+
head {
|
790
|
+
# Your existing global styles
|
791
|
+
stylesheet_link_tag "application"
|
792
|
+
|
793
|
+
# Component-specific Joys styles
|
794
|
+
pull_external_styles
|
795
|
+
|
796
|
+
# Page-specific overrides
|
797
|
+
stylesheet_link_tag "admin" if admin_page?
|
798
|
+
}
|
799
|
+
```
|
800
|
+
|
801
|
+
---
|
802
|
+
|
803
|
+
### TL;DR – Why You’ll Never Write CSS the Old Way Again
|
804
|
+
|
805
|
+
* **Scoped**: Optional prefixing prevents collisions
|
806
|
+
* **Deduped**: Automatic removal of duplicate rules
|
807
|
+
* **Ordered**: Nested components and responsive queries just work
|
808
|
+
* **Responsive**: min/max/minmax media queries built-in
|
809
|
+
* **Inline or exportable**: Works in rendering or as a static CSS file
|
810
|
+
|
811
|
+
> Write CSS where it belongs: next to your component, in pure Ruby, with **React-level convenience and performance**—all without leaving Ruby.
|
812
|
+
|
813
|
+
NOTE: This feature is completely optional. If you are using Tailwind or similar asset compiler, Joys will work just fine with that. However, the integrated css handling greatly simplifies the generation and compilation of styles, using a pure, Node-free process which has zero impact on rendering speed.
|
814
|
+
|
815
|
+
## Functional at Heart, But Ready for OOP
|
816
|
+
|
817
|
+
Joys is designed to stay out of your way. At its core, it leans functional — you define small, composable render functions instead of verbose boilerplate. But if your project already leans heavily on OOP, Joys can fold right in, allowing you to organize views as classes with inheritance and overrides.
|
818
|
+
|
819
|
+
The DSL is the same either way — Joys just adapts.
|
820
|
+
|
821
|
+
|
822
|
+
```ruby
|
823
|
+
class BaseLayout
|
824
|
+
include Joys::Helpers
|
825
|
+
|
826
|
+
def render(&block)
|
827
|
+
html {
|
828
|
+
head { title("My App") }
|
829
|
+
body { block.call }
|
830
|
+
}
|
831
|
+
end
|
832
|
+
end
|
833
|
+
|
834
|
+
class Dashboard < BaseLayout
|
835
|
+
def render
|
836
|
+
super {
|
837
|
+
div(cs: "dashboard") {
|
838
|
+
h1("Dashboard")
|
839
|
+
p("Rendered through OOP inheritance.")
|
840
|
+
a("Logout", cs:"btn",href:"/logout")
|
841
|
+
}
|
842
|
+
}
|
843
|
+
end
|
844
|
+
end
|
845
|
+
|
846
|
+
puts Dashboard.new.render
|
847
|
+
# => <html><head><title>My App</title></head><body><div class="dashboard"><h1>Dashboard</h1><p>Rendered through OOP inheritance.</p><a class="btn" href="/logout">Logout</a></div></body></html>
|
848
|
+
```
|
849
|
+
|
850
|
+
You can pick the style that fits your project, or even mix them.
|
851
|
+
|
852
|
+
---
|
853
|
+
## 🧩 Bespoke Page Architecture: Content-Driven Flexibility
|
854
|
+
|
855
|
+
**Perfect for landing pages, marketing campaigns, and any layout requiring easy reorganization.**
|
856
|
+
|
857
|
+
Modular Page Architecture lets you build pages as Ruby modules with clean data separation, allowing rapid section reorganization without touching template code.
|
858
|
+
|
859
|
+
### The Pattern: Data + Methods + Flexible Assembly
|
860
|
+
|
861
|
+
```ruby
|
862
|
+
# pages/product_launch.rb
|
863
|
+
module ProductLaunch
|
864
|
+
DATA = {
|
865
|
+
hero: {
|
866
|
+
title: "Revolutionary SaaS Platform",
|
867
|
+
cta: "Start Free Trial"
|
868
|
+
},
|
869
|
+
features: {
|
870
|
+
items: [
|
871
|
+
{ title: "Lightning Fast", desc: "10x faster" },
|
872
|
+
{ title: "Secure", desc: "Bank-level security" }
|
873
|
+
]
|
874
|
+
}
|
875
|
+
}.freeze
|
876
|
+
|
877
|
+
def self.render
|
878
|
+
Joys.define(:page, :product_launch) do
|
879
|
+
layout(:main) {
|
880
|
+
push(:main) {
|
881
|
+
raw hero(DATA[:hero])
|
882
|
+
raw features(DATA[:features])
|
883
|
+
|
884
|
+
# Custom sections between data-driven ones
|
885
|
+
div(cs: "demo") {
|
886
|
+
h2("See It In Action")
|
887
|
+
comp(:demo_video)
|
888
|
+
}
|
889
|
+
|
890
|
+
comp(:testimonials) # Reusable global component
|
891
|
+
}
|
892
|
+
}
|
893
|
+
end
|
894
|
+
end
|
895
|
+
|
896
|
+
private
|
897
|
+
|
898
|
+
def self.hero(data)
|
899
|
+
Joys.html {
|
900
|
+
styles { css ".hero { background: #667eea; padding: 4rem; }" }
|
901
|
+
# All css rendered and deduplicated within page scope
|
902
|
+
section(cs: "hero") {
|
903
|
+
h1(data[:title])
|
904
|
+
a(data[:cta], href: "/signup", cs: "btn")
|
905
|
+
}
|
906
|
+
}
|
907
|
+
end
|
908
|
+
|
909
|
+
def self.features(data)
|
910
|
+
Joys.html {
|
911
|
+
section(cs: "features") {
|
912
|
+
data[:items].each { |item|
|
913
|
+
div {
|
914
|
+
h3(item[:title])
|
915
|
+
p(item[:desc])
|
916
|
+
}
|
917
|
+
}
|
918
|
+
}
|
919
|
+
}
|
920
|
+
end
|
921
|
+
end
|
922
|
+
```
|
923
|
+
|
924
|
+
### Why This Pattern Works
|
925
|
+
|
926
|
+
**📊 Content Management Made Simple**
|
927
|
+
- All page content in one DATA hash
|
928
|
+
- Easy updates without touching templates
|
929
|
+
|
930
|
+
**🎯 Effortless Reorganization**
|
931
|
+
- Move sections by reordering method calls
|
932
|
+
- Client requests become 30-second changes
|
933
|
+
|
934
|
+
**🧩 Best of All Worlds**
|
935
|
+
- Data-driven sections for consistency
|
936
|
+
- Custom HTML where needed
|
937
|
+
- Reusable global components
|
938
|
+
- Page-specific anonymous components
|
939
|
+
|
940
|
+
**⚡ Performance Benefits**
|
941
|
+
- No global registry pollution
|
942
|
+
- Page-scoped CSS compilation
|
943
|
+
- Leverages existing Joys optimizations
|
944
|
+
|
945
|
+
This pattern transforms tedious page layout management into an agile, data-driven workflow that scales beautifully.
|
946
|
+
|
947
|
+
---
|
948
|
+
|
949
|
+
## The Final Assessment: When to Use Joys
|
950
|
+
|
951
|
+
- You want **high-throughput** Ruby APIs serving HTML.
|
952
|
+
- You want **memory-tight rendering** in constrained environments.
|
953
|
+
- You want to **replace ERB** without giving up **developer happiness**.
|
954
|
+
- You want to build functional, composable user interfaces
|
955
|
+
|
956
|
+
## When Not to Use Joys
|
957
|
+
|
958
|
+
- You need **streaming HTML** (use ERB, Slim & Phlex instead)
|
959
|
+
- You want maturity and community. This library is quite new!
|
960
|
+
- You prefer HTML markup over a Ruby DSL
|
961
|
+
|
962
|
+
Read on for the full scope of features and then decide if it's worth giving Joys a try!
|
963
|
+
|
964
|
+
## Frequently Asked Questions
|
965
|
+
|
966
|
+
### What versions of Ruby are supported?
|
967
|
+
|
968
|
+
Joys is compatible with modern Ruby versions (≥2.6) and tested on recent releases for performance and feature stability.
|
969
|
+
|
970
|
+
### How does Joys handle escaping and injection attacks?
|
971
|
+
|
972
|
+
Joys defaults to HTML-escaping text, making output safe unless explicitly marked as raw. Use txt for safe user content, and raw only for trusted HTML fragments.
|
973
|
+
|
974
|
+
### Can Joys be used outside Rails?
|
975
|
+
|
976
|
+
Yes, Joys is a pure Ruby templating engine and works in any Ruby project, not just Rails. Rails integration is completely optional.
|
977
|
+
|
978
|
+
### Do Rails helpers and SafeBuffer work with Joys?
|
979
|
+
|
980
|
+
All Rails helpers, SafeBuffer, and routing helpers work natively within Joys components and pages, so migration is straightforward and full-featured.
|
981
|
+
|
982
|
+
### How are components and layouts organized?
|
983
|
+
|
984
|
+
Joys uses a global registry for components, pages, and layouts. Namespacing and convention (symbols or strings) are supported for large codebases to avoid naming collisions.
|
985
|
+
|
986
|
+
### What should I do if I see an error?
|
987
|
+
|
988
|
+
Any runtime errors or misuse are clearly displayed in the stack trace, making debugging direct and transparent. There are very few edge case errors, and all are intentionally explicit to ease troubleshooting.
|
989
|
+
|
990
|
+
### Is it really that fast, or is this just hype?
|
991
|
+
|
992
|
+
Joys is exactly as fast as advertised after much battle testing, benching and profiling it to oblivion. If you find any flaw in our testing methodology please let us know how we can improve or level the playing field.
|
993
|
+
|
994
|
+
### Why Not Just Use Phlex instead?
|
995
|
+
|
996
|
+
Truthfully, there is no compelling reason not to use Phlex. It's mature, blazing fast and smart on memory consumption. Joys is very much inspired by this framework. The APIs are strikingly similar, however Joys adopts a more functional/minimal paradigm for composition. It also comes with UI styling baked in (something not seen in any other ruby templating library).
|
997
|
+
|
998
|
+
---
|
999
|
+
|
1000
|
+
## License
|
1001
|
+
|
1002
|
+
MIT
|
1003
|
+
|
1004
|
+
---
|
1005
|
+
|
1006
|
+
Joys isn’t just a templating engine. It’s a new definition of what rendering in Ruby should feel like: fast, safe, and joyful.
|
1007
|
+
|
1008
|
+
*Built with ❤️ and a relentless focus on performance.*
|
1009
|
+
|
1010
|
+
---
|
1011
|
+
|
1012
|
+
## Thanks and Gratitude
|
1013
|
+
|
1014
|
+
Massive shout-out to the Masatoshi Seki (ERB), Daniel Mendler (Slim), Daniel Mendler (Phlex/P2), and Jonathan Gillette (Markaby). All of you have made countless lives so much easier. The ruby community thanks you for your service!
|