Skip to content

Commit 5fac401

Browse files
authored
Merge pull request #185 from joyofrails/article/dynamic-css
Adds draft article on dynamic css
2 parents 00d4d97 + 0edc5ba commit 5fac401

File tree

65 files changed

+1324
-478
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+1324
-478
lines changed
10.6 KB
Loading

app/assets/images/external-link.svg

Lines changed: 1 addition & 1 deletion
Loading
Lines changed: 121 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,122 @@
11
/*! vim syntax highlighting - from http://pygments.org/demo/134881/?style=vim */
2-
.highlight,.highlight pre,.highlight table { background:#000 !important;color:#ccc !important; }
3-
.highlight .hll { background-color:#222 !important; }
4-
.highlight .err { color:#ccc !important;border:1px solid red !important; }
5-
.highlight .o { color:#39c !important; }
6-
.highlight .cs { color:#cd0000 !important;font-weight:700 !important; }
7-
.highlight .ge { color:#ccc !important;font-style:italic !important; }
8-
.highlight .gr { color:red !important; }
9-
.highlight .go { color:#888 !important; }
10-
.highlight .gs { color:#ccc !important;font-weight:700 !important; }
11-
.highlight .gu { color:purple !important;font-weight:700 !important; }
12-
.highlight .gt { color:#04D !important; }
13-
.highlight .ne { color:#669 !important;font-weight:700 !important; }
14-
.highlight .c,.highlight .cm,.highlight .cp,.highlight .c1 { color:#0000cd !important; }
15-
.highlight .g,.highlight .l,.highlight .n,.highlight .x,.highlight .p,.highlight .ld,.highlight .na,.highlight .no,
16-
.highlight .nd,.highlight .ni,.highlight .nf,.highlight .nl,.highlight .nn,.highlight .nx,.highlight .py,
17-
.highlight .nt,.highlight .w,.highlight .h { color:#ccc !important; }
18-
.highlight .k,.highlight .kc,.highlight .kp,.highlight .kr,.highlight .ow { color:#cdcd00 !important; }
19-
.highlight .gd,.highlight .s,.highlight .sb,.highlight .sc,.highlight .sd,.highlight .s2,.highlight .se,.highlight .sh,
20-
.highlight .si,.highlight .sx,.highlight .sr,.highlight .s1,.highlight .ss { color:#cd0000 !important; }
21-
.highlight .gh,.highlight .gp { color:navy !important;font-weight:700 !important; }
22-
.highlight .gi,.highlight .kd,.highlight .kt { color:#00cd00 !important; }
23-
.highlight .kn,.highlight .m,.highlight .nb,.highlight .mf,.highlight .mh,.highlight .mi,.highlight .mo,
24-
.highlight .bp,.highlight .il { color:#cd00cd !important; }
25-
.highlight .nc,.highlight .nv,.highlight .vc,.highlight .vg,.highlight .vi { color:#00cdcd !important; }
2+
.highlight,
3+
.highlight pre,
4+
.highlight table {
5+
background: #000000 !important;
6+
color: #ccc !important;
7+
}
8+
.highlight .hll {
9+
background-color: #222 !important;
10+
}
11+
.highlight .err {
12+
color: #ccc !important;
13+
border: 1px solid red !important;
14+
}
15+
.highlight .o {
16+
color: #39c !important;
17+
}
18+
.highlight .cs {
19+
color: #cd0000 !important;
20+
font-weight: 700 !important;
21+
}
22+
.highlight .ge {
23+
color: #ccc !important;
24+
font-style: italic !important;
25+
}
26+
.highlight .gr {
27+
color: red !important;
28+
}
29+
.highlight .go {
30+
color: #888 !important;
31+
}
32+
.highlight .gs {
33+
color: #ccc !important;
34+
font-weight: 700 !important;
35+
}
36+
.highlight .gu {
37+
color: purple !important;
38+
font-weight: 700 !important;
39+
}
40+
.highlight .gt {
41+
color: #04d !important;
42+
}
43+
.highlight .ne {
44+
color: #669 !important;
45+
font-weight: 700 !important;
46+
}
47+
.highlight .c,
48+
.highlight .cm,
49+
.highlight .cp,
50+
.highlight .c1 {
51+
color: #0000cd !important;
52+
}
53+
.highlight .g,
54+
.highlight .l,
55+
.highlight .n,
56+
.highlight .x,
57+
.highlight .p,
58+
.highlight .ld,
59+
.highlight .na,
60+
.highlight .no,
61+
.highlight .nd,
62+
.highlight .ni,
63+
.highlight .nf,
64+
.highlight .nl,
65+
.highlight .nn,
66+
.highlight .nx,
67+
.highlight .py,
68+
.highlight .nt,
69+
.highlight .w,
70+
.highlight .h {
71+
color: #ccc !important;
72+
}
73+
.highlight .k,
74+
.highlight .kc,
75+
.highlight .kp,
76+
.highlight .kr,
77+
.highlight .ow {
78+
color: #cdcd00 !important;
79+
}
80+
.highlight .gd,
81+
.highlight .s,
82+
.highlight .sb,
83+
.highlight .sc,
84+
.highlight .sd,
85+
.highlight .s2,
86+
.highlight .se,
87+
.highlight .sh,
88+
.highlight .si,
89+
.highlight .sx,
90+
.highlight .sr,
91+
.highlight .s1,
92+
.highlight .ss {
93+
color: #cd0000 !important;
94+
}
95+
.highlight .gh,
96+
.highlight .gp {
97+
color: navy !important;
98+
font-weight: 700 !important;
99+
}
100+
.highlight .gi,
101+
.highlight .kd,
102+
.highlight .kt {
103+
color: #00cd00 !important;
104+
}
105+
.highlight .kn,
106+
.highlight .m,
107+
.highlight .nb,
108+
.highlight .mf,
109+
.highlight .mh,
110+
.highlight .mi,
111+
.highlight .mo,
112+
.highlight .bp,
113+
.highlight .il {
114+
color: #cd00cd !important;
115+
}
116+
.highlight .nc,
117+
.highlight .nv,
118+
.highlight .vc,
119+
.highlight .vg,
120+
.highlight .vi {
121+
color: #00cdcd !important;
122+
}

app/content/models/article_page.rb

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,26 @@ class ArticlePage < Sitepress::Model
88
delegate :mime_type, :handler, to: :page
99

1010
def self.published
11-
all
12-
.filter { |article| article.published.present? }
11+
all.filter(&:published?)
1312
.sort { |a, b| b.published_on <=> a.published_on } # DESC order
1413
end
1514

1615
def self.draft
17-
all.filter { |article| article.published.blank? }
16+
all.filter(&:draft?)
1817
end
1918

19+
def published?
20+
published_on.presence && published_on <= Date.today
21+
end
22+
23+
def draft? = !published?
24+
2025
def persisted?
2126
false
2227
end
2328

2429
def published_on
25-
published.to_date
30+
published&.to_date
2631
end
2732
alias_method :published_at, :published_on
2833
alias_method :created_at, :published_on

app/content/pages/articles/custom-color-schemes-with-ruby-on-rails.html.mdrb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
title: Custom color schemes with Ruby on Rails
2+
title: Custom Color Schemes with Ruby on Rails
33
author: Ross Kaffenberger
44
layout: article
55
description: You can edit the color scheme of this website right in content of this blog post. Play with the controls while we highlight the benefits of Rails, Hotwire, and CSS variables.
@@ -16,7 +16,7 @@ This blog post uses Rails and Hotwire to let you edit the color scheme of this s
1616

1717
Give it a try.
1818

19-
<%= turbo_frame_tag "color-scheme-form", src: settings_color_scheme_path(custom_color_scheme_params), class: "grid-cols-12 lg:grid-cols-12" %>
19+
<%= turbo_frame_tag "color-scheme-form", src: settings_color_scheme_path(custom_color_scheme_params), class: "grid-cols-12 lg:grid-cols-12 m-bs-m" %>
2020
<noscript>
2121
JavaScript not enabled? Go to the <%= link_to "color scheme demo", settings_color_scheme_path(custom_color_scheme_params) %>. Then come back when you’re done.
2222
</noscript>
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
---
2+
title: How to Render CSS Dynamically in Rails
3+
author: Ross Kaffenberger
4+
layout: article
5+
summary: Rails is not just for HTML over the wire. This post demonstrates how and why you might use Rails for delivering CSS on the fly too.
6+
description: Rails is not just for HTML over the wire. This post demonstrates how and why you might use Rails for delivering CSS on the fly too.
7+
published: '2024-08-12'
8+
uuid: 3be769c4-6a2a-4cfb-8008-94046b952aa6
9+
image: articles/how-to-render-css-dynamically-in-rails/placeholder.jpg
10+
meta_image: articles/how-to-render-css-dynamically-in-rails/placeholder.jpg
11+
tags:
12+
- Rails
13+
---
14+
15+
<% @color_scheme_blue_chill = ColorScheme.find_by(name: 'Custom Blue Chill') || ColorScheme.last %>
16+
17+
Let‘s talk about how to render CSS dynamically with Ruby on Rails.
18+
19+
Most of the time, you might think of CSS as a static asset. But sometimes you want _dynamic_ CSS. Maybe end-user preferences, results of an A/B test, or some organizational data in the system should determine what styles to present.
20+
21+
In this post, we‘ll talk about some techniques and considerations to accomplish this in Rails. In my [previous article](/articles/color-schemes-with-ruby-on-rails), I used Hotwire to let you, the reader, preview and safe different color schemes for this site. As a recap, here‘s a slim demo so you can see how it works:
22+
23+
<%= turbo_frame_tag "color-scheme-preview", src: preview_settings_color_scheme_path(custom_color_scheme_params), class: "grid-cols-12 lg:grid-cols-12 m-bs-m" %>
24+
<noscript>
25+
JavaScript not enabled? Go to the <%= link_to "color scheme demo", settings_color_scheme_path(custom_color_scheme_params) %>. Then come back when you’re done.
26+
</noscript>
27+
28+
## ERB isn‘t just for HTML
29+
30+
The color scheme preview relies on Ruby embedded in HTML templates on the server to render CSS into a `<style>` tag.
31+
32+
While most of your CSS likely should be rendered in a bundled CSS file (or files), like `application.css`, it may make sense for small bits of custom CSS to be rendered inline in HTML. Your static CSS files will be served from your web server or likely a Content Delivery Network (CDN) depending on your application setup, which skips your Rails application logic.
33+
34+
That‘s true for most of the CSS in Joy of Rails too. But, to make the the color scheme preview, I‘ve mixed in some dynamic CSS into the Hotwire interaction. When you make a color scheme selection, a request is issued to update the color scheme preview. The endpoint returns an HTML response with a [Turbo Frame](https://turbo.hotwired.dev/handbook/frames) containing a `<style>` tag. Hotwire swaps out the portion containing the Turbo Frame including the new styles. As an example, here‘s part of the HTML response when you select **Blue Chill**:
35+
36+
```html
37+
<html>
38+
<head></head>
39+
<body>
40+
<!-- ... -->
41+
<section class="...">
42+
<turbo-frame id="color-scheme-preview">
43+
<style>
44+
:root {
45+
--color-custom-blue-chill-50: hsla(180, 53%, 97%, 1);
46+
--color-custom-blue-chill-100: hsla(178, 64%, 89%, 1);
47+
--color-custom-blue-chill-200: hsla(179, 64%, 78%, 1);
48+
--color-custom-blue-chill-300: hsla(182, 58%, 64%, 1);
49+
--color-custom-blue-chill-400: hsla(183, 49%, 50%, 1);
50+
--color-custom-blue-chill-500: hsla(184, 61%, 37%, 1);
51+
--color-custom-blue-chill-600: hsla(185, 64%, 32%, 1);
52+
--color-custom-blue-chill-700: hsla(186, 59%, 26%, 1);
53+
--color-custom-blue-chill-800: hsla(187, 53%, 22%, 1);
54+
--color-custom-blue-chill-900: hsla(187, 46%, 19%, 1);
55+
--color-custom-blue-chill-950: hsla(189, 65%, 10%, 1);
56+
}
57+
58+
:root {
59+
--my-color-50: var(--color-custom-blue-chill-50);
60+
--my-color-100: var(--color-custom-blue-chill-100);
61+
--my-color-200: var(--color-custom-blue-chill-200);
62+
--my-color-300: var(--color-custom-blue-chill-300);
63+
--my-color-400: var(--color-custom-blue-chill-400);
64+
--my-color-500: var(--color-custom-blue-chill-500);
65+
--my-color-600: var(--color-custom-blue-chill-600);
66+
--my-color-700: var(--color-custom-blue-chill-700);
67+
--my-color-800: var(--color-custom-blue-chill-800);
68+
--my-color-900: var(--color-custom-blue-chill-900);
69+
--my-color-950: var(--color-custom-blue-chill-950);
70+
}
71+
</style>
72+
<!-- ... -->
73+
</turbo-frame>
74+
</section>
75+
</body>
76+
</html>
77+
```
78+
79+
As you can see, the color scheme for Joy of Rails is built on [CSS Variables](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties). Using CSS variables for dynamic CSS isn‘t required but sure makes the job of updating page styles easier. We can set values of CSS variables to new values by rendering new CSS or by JavaScript manipulation; this makes CSS variables an ideal choice for dynamic CSS techniques like those described in this article.
80+
81+
The partial that renders CSS in the HTML response looks something like this:
82+
83+
<%= render CodeBlock::AppFile.new("app/views/application/theme/\_color.html.erb", language: "erb", revision: "9852f2ccf65401848c19a6eabd1eb74fca49c789") %>
84+
85+
The controller helper method `custom_color_scheme?` looks for the presence of a color scheme id in `params` or the `session` and `find_color_scheme` makes a database query to find a `ColorScheme` record if needed.
86+
87+
`ColorSchemes::Css` is a simple [Phlex](https://www.phlex.fun/) component. Here‘s what it looks like:
88+
89+
<%= render CodeBlock::AppFile.new("app/views/components/color_schemes/css.rb", language: "ruby") %>
90+
91+
Phlex is a Ruby gem for building object-oriented HTML components. It‘s an alternative to ERB templates. Phlex suits my preferences in this case, but it isn‘t a requirement for dynamic CSS. You could easily write this in an ERB template instead. It might look something like this:
92+
93+
```erb
94+
<%
95+
color_name = @color_scheme.name.parameterize
96+
# #to_hsla defined as a helper method
97+
%>
98+
<style>
99+
:root {
100+
<%= @color_scheme.weights.map { |weight, color| "--color-#{color_name}-#{weight}: #{to_hsla(color)};" }.join("\n\s\s") %>
101+
102+
<% if @my_theme %>
103+
<%= @color_scheme.weights.map { |weight, color| "--my-color-#{weight}: var(--color-#{color_name}-#{weight});" }.join("\n\s\s") %>
104+
<% end %>
105+
}
106+
</style>
107+
```
108+
109+
The key point here is that we can use logic in Rails templates or components for rendering key bits of dynamic CSS at the time of the request, just as you can for HTML. When combined with Hotwire, this enables server-driven interactive styles like my color scheme preview.
110+
111+
## Controller actions aren’t just for HTML either
112+
113+
Rails controllers can do more than just HTML. In fact, Rails controller actions support over thirty [MIME types](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types) by default (as of Rails 7.1).
114+
115+
```ruby
116+
$ bin/rails s
117+
118+
irb> Mime::SET.collect(&:to_s)
119+
=>
120+
["text/html", "text/plain", "text/javascript", "text/css", "text/calendar", "text/csv", "text/vcard", "text/vtt", "image/png", "image/jpeg", "image/gif", "image/bmp", "image/tiff", "image/svg+xml", "image/webp", "video/mpeg", "audio/mpeg", "audio/ogg", "audio/aac", "video/webm", "video/mp4", "font/otf", "font/ttf", "font/woff", "font/woff2", "application/xml", "application/rss+xml", "application/atom+xml", "application/x-yaml", "multipart/form-data", "application/x-www-form-urlencoded", "application/json", "application/pdf", "application/zip", "application/gzip", "text/vnd.turbo-stream.html"]
121+
```
122+
123+
In Rails `mime_types.rb`, we can see the registered MIME types with each of their recognized extensions.
124+
125+
That `:css` in the MIME type registry is one way to instruct Rails controller behavior with `respond_to`.
126+
127+
Using `respond_to` in a controller allows you to define responses and logic based on the requested format. Here‘s how I take advantage of this behavior in `ColorSchemesController#show`:
128+
129+
<%= render CodeBlock::AppFile.new("app/controllers/color_schemes_controller.rb", lines: [1, 8..17], language: "ruby", revision: "bd53833b03c7f957546e5a9031643d3beb179beb") %>
130+
131+
This controller action will respond differently for HTML and CSS requests. To demonstrate the difference, I've inserted two iframes for `ColorSchemesController#show` below:
132+
133+
#### Iframe for HTML request
134+
135+
Below you‘ll see an `iframe` with `src` set to request the **Blue Chill** color scheme as `text/html`.
136+
137+
<%= tag.iframe src: color_scheme_path(@color_scheme_blue_chill) %>
138+
139+
Here’s the code for the `iframe`:
140+
141+
```erb:{"show_header": false}
142+
<%= tag.iframe src: color_scheme_path(@color_scheme_blue_chill) %>
143+
```
144+
145+
#### Iframe for CSS request
146+
147+
And here you‘ll see an `iframe` with `src` set to request the **Blue Chill** color scheme as `text/css`.
148+
149+
<%= tag.iframe src: color_scheme_path(@color_scheme_blue_chill, format: :css) %>
150+
151+
Here’s the code for the `iframe`:
152+
153+
```erb:{"show_header": false}
154+
<%= tag.iframe src: color_scheme_path(@color_scheme_blue_chill, format: :css) %>
155+
```
156+
157+
Behold: dynamic css from a controller action!
158+
159+
We can use this endpoint in a stylesheet `<link>` tag just like any other static CSS URL:
160+
161+
```html
162+
<%= stylesheet_link_tag color_scheme_path(@color_scheme, format: :css) %>
163+
```
164+
165+
A downside of moving our dynamic CSS to a separate controller action is that it requires an additional HTTP request. This could be an issue for a highly interactive user experience. But on the other hand, using a separate request allows you to take advantage of Rails [conditional GET features](https://guides.rubyonrails.org/v3.2/caching_with_rails.html#conditional-get-support)
166+
167+
> Conditional GETs are a feature of the HTTP specification that provide a way for web servers to tell browsers that the response to a GET request hasn't changed since the last request and can be safely pulled from the browser cache.
168+
>
169+
> [Source: Rails guides](https://guides.rubyonrails.org/caching_with_rails.html#conditional-get-support)
170+
171+
Below, I‘ve modified the `ColorSchemesController#show` to use the `stale?` method to enable conditional GET:
172+
173+
<%= render CodeBlock::AppFile.new("app/controllers/color_schemes_controller.rb", lines: [1, 8..19], language: "ruby", revision: "c6f970e6c95787ab66c75f9d230734710084c9d2") %>
174+
175+
This method will calculate a value for `Etag` or `Last-Modified` response headers and set the status to `304 Not Modified` if request headers match and the server doesn’t need to render anything.
176+
177+
In short, dynamic CSS combined with a conditional GET allows you to leverage put Ruby logic behind your stylesheet link tags in a performant manner.
178+
179+
## Recap
180+
181+
Rails provides us with all sorts of [sharp knives](https://rubyonrails.org/doctrine#provide-sharp-knives) especially it comes to dynamic rendering in various formats, like CSS.
182+
183+
We can allow for real-time style changes based on user preferences or application state to enable user- or context-specific styling—like [custom color schemes](/settings/color_schemes) for your application.
184+
185+
We can render CSS in `<style>` tags using embedded Ruby. This approach works well for small bits of CSS and may save bandwidth and latency without having to make additional HTTP requests
186+
187+
Rendering CSS in a controller isn‘t as crazy as it sounds. This approach offers flexibility and is primed for HTTP caching through conditional GET support in Rails.
188+
189+
---
190+
191+
Did you find this article helpful? How are you doing dynamic CSS? Let me know on [Twitter](https://x.com/rossta), [Mastodon](https://ruby.social/@rossta), or [send me an email](mailto:[email protected]). You can check out the [source code for Joy of Rails on Github](https://github.com/joyofrails/joyofrails.com). And... you can [subscribe](#newsletter-signup) to my newsletter to get notified of new content.
192+
193+
Until next time, have fun!

0 commit comments

Comments
 (0)