view_component-scoped_styles 0.4.1 → 0.6.0
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/README.md +79 -8
- data/lib/generators/view_component/scoped_styles/templates/view_component_scoped_styles.rb.tt +7 -1
- data/lib/generators/view_component/stylesheet/stylesheet_generator.rb +77 -0
- data/lib/generators/view_component/stylesheet/templates/component.css.tt +3 -0
- data/lib/view_component/scoped_styles/component_generator_hook.rb +14 -0
- data/lib/view_component/scoped_styles/concern.rb +96 -15
- data/lib/view_component/scoped_styles/configuration.rb +14 -3
- data/lib/view_component/scoped_styles/css_class_prefix.rb +38 -0
- data/lib/view_component/scoped_styles/railtie.rb +4 -0
- data/lib/view_component/scoped_styles/version.rb +1 -1
- data/lib/view_component/scoped_styles.rb +2 -0
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c1612f1fbc0f2b03fa6930fd04b2749a914795ec7c4c23dd3d9f3a6b09ac0666
|
|
4
|
+
data.tar.gz: 443a89edf71c93e6af8bea761c77c4ed1c9d8788005fcfe21a90cf8371bd118f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 84893c6c973b772ed5780eebb9400201ea01b1e3e9db1489dff02eb03ce7e9c5591dd6ab34cf6528c63136ddfe69a392bd6def73904057389493115f35d6c487
|
|
7
|
+
data.tar.gz: 69d6aad3940294c2a3a809926594a9f081115c244d6fe19ef23f3ccd8126ff9fc87d5bb81e9c50edeb046c936e61a03eb387255bbf5d014ebad42c08c8909628
|
data/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Scoped, colocated CSS for [ViewComponent](https://viewcomponent.org/).
|
|
|
4
4
|
|
|
5
5
|
Avoids collisions by rewriting class selectors to stable, content-derived names.
|
|
6
6
|
|
|
7
|
-
E.g. `.button` becomes `.
|
|
7
|
+
E.g. `.button` becomes `.button_a1b2c3d4`
|
|
8
8
|
|
|
9
9
|
## Table of Contents
|
|
10
10
|
|
|
@@ -60,6 +60,21 @@ CSS can be written in two ways:
|
|
|
60
60
|
|
|
61
61
|
Learn more about sidecar [here](https://viewcomponent.org/guide/generators.html#place-the-view-in-a-sidecar-directory).
|
|
62
62
|
|
|
63
|
+
```bash
|
|
64
|
+
bin/rails generate view_component:component Example title --sidecar
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
With automatic stylesheet generation enabled in `config/application.rb`, `config/ENVIRONMENT.rb`, or wherever you configure ViewComponent:
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
# Ensure sidebar is enabled
|
|
71
|
+
config.view_component.generate.sidecar = true
|
|
72
|
+
# Then generate stylesheets via
|
|
73
|
+
config.view_component.generate.stylesheet = true
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`view_component:component` also creates the sidecar `.css` file, adds `include ViewComponent::ScopedStyles` to the component class, and sets `class="<%= component_class %>"` on the generated ERB template. You can pass `--no-stylesheet` to skip CSS generation for a single component.
|
|
77
|
+
|
|
63
78
|
```bash
|
|
64
79
|
bin/rails generate view_component:component Example title --sidecar
|
|
65
80
|
|
|
@@ -68,10 +83,12 @@ bin/rails generate view_component:component Example title --sidecar
|
|
|
68
83
|
create test/components/example_component_test.rb
|
|
69
84
|
invoke erb
|
|
70
85
|
create app/components/example_component/example_component.html.erb
|
|
86
|
+
invoke stylesheet
|
|
87
|
+
create app/components/example_component/example_component.css
|
|
71
88
|
|
|
72
89
|
```
|
|
73
90
|
|
|
74
|
-
|
|
91
|
+
Or add a matching stylesheet manually in the sidecar directory:
|
|
75
92
|
|
|
76
93
|
```css
|
|
77
94
|
/* app/components/example_component/example_component.css */
|
|
@@ -161,22 +178,76 @@ end
|
|
|
161
178
|
</div>
|
|
162
179
|
```
|
|
163
180
|
|
|
164
|
-
|
|
181
|
+
You can also reference scoped classes from another component by passing the component class with `from:`:
|
|
182
|
+
|
|
183
|
+
```erb
|
|
184
|
+
<div class="<%= component_class(from: ButtonComponent) %>">
|
|
185
|
+
Button root class
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
<div class="<%= component_class("icon", from: ButtonComponent) %>">
|
|
189
|
+
Button icon class
|
|
190
|
+
</div>
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Inside sidecar stylesheets or `styles` blocks, use `:component(...)` as a compile-time reference to another component's scoped classes:
|
|
194
|
+
|
|
195
|
+
```css
|
|
196
|
+
.component:has(:component(ButtonComponent, icon)) {
|
|
197
|
+
padding-inline-end: 2rem;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
:where(:component(CardComponent), :component(PanelComponent)) .title {
|
|
201
|
+
margin: 0;
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
The first argument is the component constant, and the optional second argument is the class selector without a leading dot. When the class name is omitted, the referenced component's root class is used. `:component(...)` is replaced while scoped CSS is generated, so it can be used inside more complex selectors like `:where()`, `:is()`, `:not()`, and `:has()`.
|
|
206
|
+
|
|
207
|
+
Scoped class names are prefixed by default using the class name (e.g. `.component` → `component_a1b2c3d4`). Set a global prefix in configuration, or override per component with `css_class_prefix`.
|
|
208
|
+
|
|
209
|
+
The prefix is a template string. Use `{class_name}` for the CSS class being scoped and `{component_name}` for the component name (namespaces joined by `/`, with a trailing `Component` suffix removed):
|
|
210
|
+
|
|
211
|
+
```ruby
|
|
212
|
+
ViewComponent::ScopedStyles.configure do |config|
|
|
213
|
+
config.css_class_prefix = "{class_name}_" # default
|
|
214
|
+
end
|
|
215
|
+
```
|
|
165
216
|
|
|
166
217
|
```ruby
|
|
167
218
|
class ExampleComponent < ViewComponent::Base
|
|
168
219
|
include ViewComponent::ScopedStyles
|
|
169
220
|
|
|
170
|
-
css_class_prefix "vc-"
|
|
221
|
+
css_class_prefix "vc-{class_name}_"
|
|
222
|
+
|
|
223
|
+
styles do
|
|
224
|
+
<<~CSS
|
|
225
|
+
.component { ... } # becomes .vc-component_a1b2c3d4 in components.scoped.css
|
|
226
|
+
CSS
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Namespaced components can include the component name in the prefix:
|
|
232
|
+
|
|
233
|
+
```ruby
|
|
234
|
+
class Admin::UserCardComponent < ViewComponent::Base
|
|
235
|
+
include ViewComponent::ScopedStyles
|
|
236
|
+
|
|
237
|
+
css_class_prefix "{component_name}_{class_name}_"
|
|
171
238
|
|
|
172
239
|
styles do
|
|
173
240
|
<<~CSS
|
|
174
|
-
.component { ... }
|
|
241
|
+
.component { ... }
|
|
242
|
+
# Admin/UserCardComponent_component_a1b2c3d4 in templates;
|
|
243
|
+
# Admin\/UserCardComponent_component_a1b2c3d4 in components.scoped.css
|
|
175
244
|
CSS
|
|
176
245
|
end
|
|
177
246
|
end
|
|
178
247
|
```
|
|
179
248
|
|
|
249
|
+
**Upgrading from 0.4.x:** set `config.css_class_prefix = "c-"` (and per-component overrides) to keep the previous `c-a1b2c3d4` class names without updating templates.
|
|
250
|
+
|
|
180
251
|
### Ignoring classes
|
|
181
252
|
|
|
182
253
|
Ignored classes are left unchanged in generated CSS:
|
|
@@ -242,8 +313,8 @@ ViewComponent::ScopedStyles.configure do |config|
|
|
|
242
313
|
# Optional @layer name for the bundled scoped stylesheet (e.g. "components"). Default: nil.
|
|
243
314
|
config.components_layer = nil
|
|
244
315
|
|
|
245
|
-
# Prefix for scoped class names
|
|
246
|
-
config.css_class_prefix = "
|
|
316
|
+
# Prefix for scoped class names. Supports {component_name} and {class_name}. Default: "{class_name}_"
|
|
317
|
+
config.css_class_prefix = "{class_name}_"
|
|
247
318
|
end
|
|
248
319
|
```
|
|
249
320
|
|
|
@@ -253,7 +324,7 @@ end
|
|
|
253
324
|
| `assets_path` | `"app/assets/stylesheets"` | Directory where the bundled scoped stylesheet is written, relative to `Rails.root`. |
|
|
254
325
|
| `stylesheet_name` | `"components.scoped.css"` | Filename of the bundled scoped stylesheet within `assets_path`. |
|
|
255
326
|
| `components_layer` | `nil` | When set, wraps generated CSS in `@layer <name> { ... }` for cascade control. |
|
|
256
|
-
| `css_class_prefix` | `"
|
|
327
|
+
| `css_class_prefix` | `"{class_name}_"` | Prefix template for scoped class names. Supports `{class_name}` and `{component_name}` (namespaces joined by `/`, `Component` suffix stripped; e.g. `.component` → `component_a1b2c3d4`). Use `"c-"` to match 0.4.x behavior. |
|
|
257
328
|
|
|
258
329
|
## Related projects
|
|
259
330
|
|
data/lib/generators/view_component/scoped_styles/templates/view_component_scoped_styles.rb.tt
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# Optional ViewComponent generator settings (in config/application.rb, config/ENVIRONMENT.rb, or wherever you configure ViewComponent):
|
|
4
|
+
# Ensure sidebar is enabled
|
|
5
|
+
# config.view_component.generate.sidecar = true
|
|
6
|
+
# Then generate stylesheets via
|
|
7
|
+
# config.view_component.generate.stylesheet = true
|
|
8
|
+
|
|
3
9
|
ViewComponent::ScopedStyles.configure do |config|
|
|
4
10
|
# Where ViewComponent classes live (relative to Rails.root). Default: "app/components"
|
|
5
11
|
config.components_path = <%= components_path_expression %>
|
|
@@ -13,6 +19,6 @@ ViewComponent::ScopedStyles.configure do |config|
|
|
|
13
19
|
# Optional @layer name for the bundled scoped stylesheet (e.g. "components"). Default: nil.
|
|
14
20
|
config.components_layer = <%= components_layer_value %>
|
|
15
21
|
|
|
16
|
-
# Prefix for scoped class names
|
|
22
|
+
# Prefix for scoped class names. Supports {component_name} and {class_name} variables. Default: "{class_name}_"
|
|
17
23
|
config.css_class_prefix = <%= css_class_prefix_value %>
|
|
18
24
|
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "generators/view_component/abstract_generator"
|
|
5
|
+
|
|
6
|
+
module ViewComponent
|
|
7
|
+
module Generators
|
|
8
|
+
class StylesheetGenerator < ::Rails::Generators::NamedBase
|
|
9
|
+
include ViewComponent::AbstractGenerator
|
|
10
|
+
|
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
|
12
|
+
class_option :sidecar, type: :boolean, default: false
|
|
13
|
+
class_option :skip_suffix, type: :boolean, default: false
|
|
14
|
+
|
|
15
|
+
def create_stylesheet
|
|
16
|
+
template "component.css", stylesheet_destination
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def inject_scoped_styles_module
|
|
20
|
+
return unless File.exist?(absolute_component_rb_path)
|
|
21
|
+
|
|
22
|
+
content = File.read(absolute_component_rb_path)
|
|
23
|
+
return if content.include?("ViewComponent::ScopedStyles")
|
|
24
|
+
|
|
25
|
+
class_name_pattern = component_class_name.split("::").last
|
|
26
|
+
match = content.match(/^(\s*)class #{Regexp.escape(class_name_pattern)}\b[^\n]*\n/)
|
|
27
|
+
return unless match
|
|
28
|
+
|
|
29
|
+
indent = "#{match[1]} "
|
|
30
|
+
inject_into_file component_rb_path,
|
|
31
|
+
"#{indent}include ViewComponent::ScopedStyles\n",
|
|
32
|
+
after: match[0]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def update_view_template
|
|
36
|
+
return if options["inline"] || options["call"]
|
|
37
|
+
|
|
38
|
+
path = view_template_path
|
|
39
|
+
return unless File.exist?(File.join(destination_root, path))
|
|
40
|
+
|
|
41
|
+
gsub_file path, /<div/, '<div class="<%= component_class %>"', verbose: false
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def absolute_component_rb_path
|
|
47
|
+
File.join(destination_root, component_rb_path)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def component_file_name
|
|
51
|
+
"#{file_name}#{"_component" unless options[:skip_suffix]}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def component_class_name
|
|
55
|
+
"#{class_name}#{"Component" unless options[:skip_suffix]}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def component_rb_path
|
|
59
|
+
File.join(component_path, class_path, "#{component_file_name}.rb")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def stylesheet_destination
|
|
63
|
+
if sidecar?
|
|
64
|
+
File.join(
|
|
65
|
+
component_path, class_path, component_file_name, "#{component_file_name}.css"
|
|
66
|
+
)
|
|
67
|
+
else
|
|
68
|
+
File.join(component_path, class_path, "#{component_file_name}.css")
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def view_template_path
|
|
73
|
+
File.join(destination_directory, "#{destination_file_name}.html.erb")
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "generators/view_component/component/component_generator"
|
|
4
|
+
|
|
5
|
+
module ViewComponent
|
|
6
|
+
module Generators
|
|
7
|
+
class ComponentGenerator
|
|
8
|
+
class_option :stylesheet, type: :boolean,
|
|
9
|
+
default: ViewComponent::Base.config.generate.stylesheet
|
|
10
|
+
|
|
11
|
+
hook_for :stylesheet, type: :boolean
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -8,6 +8,15 @@ module ViewComponent
|
|
|
8
8
|
|
|
9
9
|
CACHED_VARIABLES = %i[@component_styles @component_id @component_class_map].freeze
|
|
10
10
|
CLASS_SELECTOR_PATTERN = /\.([a-zA-Z_][\w-]*)\b/
|
|
11
|
+
COMPONENT_REFERENCE_PATTERN = /
|
|
12
|
+
:component\(
|
|
13
|
+
\s*
|
|
14
|
+
(?<component_name>[A-Za-z_]\w*(?:::[A-Za-z_]\w*)*)
|
|
15
|
+
\s*
|
|
16
|
+
(?:,\s*(?<css_class>[a-zA-Z_][\w-]*)\s*)?
|
|
17
|
+
\)
|
|
18
|
+
/x
|
|
19
|
+
COMPONENT_RESOLUTION_STACK_KEY = :view_component_scoped_styles_component_resolution_stack
|
|
11
20
|
|
|
12
21
|
# Default root class for +component_class+ when it matches a selector in the CSS.
|
|
13
22
|
COMPONENT_CSS_CLASS = "component".freeze
|
|
@@ -52,13 +61,14 @@ module ViewComponent
|
|
|
52
61
|
register_styles_if_rails_loaded
|
|
53
62
|
end
|
|
54
63
|
|
|
55
|
-
# Sets the prefix for scoped class names on this component
|
|
64
|
+
# Sets the prefix template for scoped class names on this component.
|
|
56
65
|
#
|
|
57
66
|
# Overrides {ViewComponent::ScopedStyles.configuration}.css_class_prefix for this component.
|
|
67
|
+
# Supports +{component_name}+ and +{class_name}+ template variables.
|
|
58
68
|
#
|
|
59
69
|
# Clears cached generated styles when +prefix+ changes.
|
|
60
70
|
#
|
|
61
|
-
# @param prefix [String, nil] prefix without a hash suffix (e.g. +"
|
|
71
|
+
# @param prefix [String, nil] prefix template without a hash suffix (e.g. +"{class_name}_"+)
|
|
62
72
|
def css_class_prefix(prefix = nil)
|
|
63
73
|
if prefix
|
|
64
74
|
const_set(:CSS_CLASS_PREFIX, prefix.to_s)
|
|
@@ -114,13 +124,26 @@ module ViewComponent
|
|
|
114
124
|
end
|
|
115
125
|
|
|
116
126
|
def generate_component_styles
|
|
127
|
+
pushed = false
|
|
128
|
+
|
|
129
|
+
if component_resolution_stack.include?(self)
|
|
130
|
+
names = (component_resolution_stack + [self]).map { |component| component.name || component.inspect }
|
|
131
|
+
raise ArgumentError, "Circular scoped style component reference: #{names.join(" -> ")}"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
component_resolution_stack.push(self)
|
|
135
|
+
pushed = true
|
|
117
136
|
styles_content = generate_styles_content
|
|
118
137
|
css_classes = extract_css_classes(styles_content)
|
|
119
138
|
primary_class = primary_css_class(css_classes)
|
|
120
139
|
|
|
121
140
|
@component_id = component_id_for(primary_class, styles_content)
|
|
122
141
|
@component_class_map = build_component_class_map(styles_content, css_classes, primary_class)
|
|
123
|
-
@component_styles =
|
|
142
|
+
@component_styles = replace_component_references(
|
|
143
|
+
replace_css_classes(styles_content, @component_class_map)
|
|
144
|
+
)
|
|
145
|
+
ensure
|
|
146
|
+
component_resolution_stack.pop if pushed
|
|
124
147
|
end
|
|
125
148
|
|
|
126
149
|
def generate_styles_content
|
|
@@ -154,7 +177,20 @@ module ViewComponent
|
|
|
154
177
|
sorted_classes = scoped_map.keys.sort_by(&:length).reverse
|
|
155
178
|
|
|
156
179
|
sorted_classes.reduce(styles_content) do |content, css_class|
|
|
157
|
-
|
|
180
|
+
scoped = class_map[css_class]
|
|
181
|
+
escaped = CssClassPrefix.escape_for_css_selector(scoped)
|
|
182
|
+
content.gsub(/\.#{Regexp.escape(css_class)}\b/, ".#{escaped}")
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def replace_component_references(styles_content)
|
|
187
|
+
styles_content.gsub(COMPONENT_REFERENCE_PATTERN) do
|
|
188
|
+
component_name = Regexp.last_match[:component_name]
|
|
189
|
+
css_class = Regexp.last_match[:css_class]
|
|
190
|
+
component_class = resolve_component_reference(component_name)
|
|
191
|
+
scoped_class = scoped_component_class(component_class, css_class)
|
|
192
|
+
|
|
193
|
+
".#{CssClassPrefix.escape_for_css_selector(scoped_class)}"
|
|
158
194
|
end
|
|
159
195
|
end
|
|
160
196
|
|
|
@@ -175,7 +211,20 @@ module ViewComponent
|
|
|
175
211
|
input = is_primary ? styles_content : "#{styles_content}:#{css_class}"
|
|
176
212
|
hash = ::Digest::MD5.hexdigest(input)[0..7]
|
|
177
213
|
|
|
178
|
-
|
|
214
|
+
prefix = CssClassPrefix.interpolate(
|
|
215
|
+
scoped_css_class_prefix,
|
|
216
|
+
component_name: scoped_component_name,
|
|
217
|
+
class_name: css_class
|
|
218
|
+
)
|
|
219
|
+
"#{prefix}#{hash}"
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def scoped_component_name
|
|
223
|
+
return "" unless name
|
|
224
|
+
|
|
225
|
+
parts = name.split("::")
|
|
226
|
+
parts[-1] = parts[-1].delete_suffix("Component")
|
|
227
|
+
parts.join("/")
|
|
179
228
|
end
|
|
180
229
|
|
|
181
230
|
def scoped_css_class_prefix
|
|
@@ -186,6 +235,36 @@ module ViewComponent
|
|
|
186
235
|
end
|
|
187
236
|
end
|
|
188
237
|
|
|
238
|
+
def resolve_component_reference(component_name)
|
|
239
|
+
component_name.to_s.split("::").inject(Object) do |namespace, constant_name|
|
|
240
|
+
namespace.const_get(constant_name)
|
|
241
|
+
end
|
|
242
|
+
rescue NameError
|
|
243
|
+
raise ArgumentError, "Unable to resolve scoped style component reference: #{component_name}"
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def scoped_component_class(component_class, css_class = nil)
|
|
247
|
+
unless component_class.respond_to?(:component_styles)
|
|
248
|
+
raise ArgumentError, "#{component_class} does not include ViewComponent::ScopedStyles"
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
component_class.component_styles
|
|
252
|
+
|
|
253
|
+
if css_class
|
|
254
|
+
scoped = component_class.instance_variable_get(:@component_class_map)[css_class.to_s.delete_prefix(".")]
|
|
255
|
+
|
|
256
|
+
raise ArgumentError, "#{component_class.name || component_class} does not define .#{css_class}" unless scoped
|
|
257
|
+
|
|
258
|
+
scoped
|
|
259
|
+
else
|
|
260
|
+
component_class.instance_variable_get(:@component_id)
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def component_resolution_stack
|
|
265
|
+
Thread.current[COMPONENT_RESOLUTION_STACK_KEY] ||= []
|
|
266
|
+
end
|
|
267
|
+
|
|
189
268
|
def clear_component_style_cache
|
|
190
269
|
CACHED_VARIABLES.each do |ivar|
|
|
191
270
|
remove_instance_variable(ivar) if instance_variable_defined?(ivar)
|
|
@@ -199,28 +278,30 @@ module ViewComponent
|
|
|
199
278
|
# appears in the CSS, otherwise the first class in the stylesheet).
|
|
200
279
|
#
|
|
201
280
|
# @param name [String, Symbol] CSS class without a leading dot (e.g. +"input-box"+)
|
|
202
|
-
|
|
203
|
-
|
|
281
|
+
# @param from [Class] component class to read scoped CSS classes from
|
|
282
|
+
def component_class(name = nil, from: self.class)
|
|
283
|
+
return nil unless from.respond_to?(:component_styles)
|
|
284
|
+
return nil unless component_has_styles?(from) || component_has_stylesheet?(from)
|
|
204
285
|
|
|
205
|
-
|
|
286
|
+
from.component_styles
|
|
206
287
|
|
|
207
288
|
if name
|
|
208
|
-
class_map =
|
|
289
|
+
class_map = from.instance_variable_get(:@component_class_map)
|
|
209
290
|
class_map[name.to_s.delete_prefix(".")]
|
|
210
291
|
else
|
|
211
|
-
|
|
292
|
+
from.instance_variable_get(:@component_id)
|
|
212
293
|
end
|
|
213
294
|
end
|
|
214
295
|
|
|
215
296
|
private
|
|
216
297
|
|
|
217
|
-
def component_has_stylesheet?
|
|
218
|
-
|
|
298
|
+
def component_has_stylesheet?(component_class = self.class)
|
|
299
|
+
component_class.has_stylesheet?
|
|
219
300
|
end
|
|
220
301
|
|
|
221
|
-
def component_has_styles?
|
|
222
|
-
|
|
223
|
-
|
|
302
|
+
def component_has_styles?(component_class = self.class)
|
|
303
|
+
component_class.instance_variable_defined?(:@styles_block) &&
|
|
304
|
+
component_class.instance_variable_get(:@styles_block)
|
|
224
305
|
end
|
|
225
306
|
end
|
|
226
307
|
end
|
|
@@ -36,9 +36,20 @@ module ViewComponent
|
|
|
36
36
|
# @return [String, nil] default: +nil+ (no layer wrapper)
|
|
37
37
|
attr_accessor :components_layer
|
|
38
38
|
|
|
39
|
-
# Prefix prepended to scoped class names
|
|
39
|
+
# Prefix prepended to scoped class names.
|
|
40
40
|
#
|
|
41
|
-
#
|
|
41
|
+
# Supports template variables:
|
|
42
|
+
#
|
|
43
|
+
# * +{component_name}+ — component name with namespaces joined by +/+,
|
|
44
|
+
# and a trailing +Component+ suffix removed
|
|
45
|
+
# * +{class_name}+ — the CSS class being scoped
|
|
46
|
+
#
|
|
47
|
+
# Example: +"{class_name}_"+ with +.component+ → +"component_a1b2c3d4"+.
|
|
48
|
+
#
|
|
49
|
+
# To maintain compatibility with versions < 0.5.0 and avoid rewriting
|
|
50
|
+
# existing stylesheets, set this to +"c-"+.
|
|
51
|
+
#
|
|
52
|
+
# @return [String] default: +"{class_name}_"+
|
|
42
53
|
attr_accessor :css_class_prefix
|
|
43
54
|
|
|
44
55
|
def initialize
|
|
@@ -46,7 +57,7 @@ module ViewComponent
|
|
|
46
57
|
@assets_path = File.join("app", "assets", "stylesheets")
|
|
47
58
|
@stylesheet_name = "components.scoped.css"
|
|
48
59
|
@components_layer = nil
|
|
49
|
-
@css_class_prefix = "
|
|
60
|
+
@css_class_prefix = "{class_name}_"
|
|
50
61
|
end
|
|
51
62
|
end
|
|
52
63
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ViewComponent
|
|
4
|
+
module ScopedStyles
|
|
5
|
+
# Interpolates +css_class_prefix+ template variables and escapes scoped class
|
|
6
|
+
# names for use in compiled CSS selectors.
|
|
7
|
+
class CssClassPrefix
|
|
8
|
+
VARIABLES = %w[component_name class_name].freeze
|
|
9
|
+
VARIABLE_PATTERN = /\{(#{VARIABLES.join("|")})\}/
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
# Replaces +{component_name}+ and +{class_name}+ in +template+.
|
|
13
|
+
#
|
|
14
|
+
# @param template [String] prefix template (e.g. +"{class_name}_"+)
|
|
15
|
+
# @param component_name [String] component name with namespaces joined by +/+,
|
|
16
|
+
# and a trailing +Component+ suffix removed
|
|
17
|
+
# @param class_name [String] CSS class being scoped
|
|
18
|
+
# @return [String]
|
|
19
|
+
def interpolate(template, component_name:, class_name:)
|
|
20
|
+
template.gsub(VARIABLE_PATTERN) do
|
|
21
|
+
case ::Regexp.last_match(1)
|
|
22
|
+
when "component_name" then component_name
|
|
23
|
+
when "class_name" then class_name
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Escapes characters that are invalid in unescaped CSS class selectors.
|
|
29
|
+
#
|
|
30
|
+
# @param class_name [String]
|
|
31
|
+
# @return [String]
|
|
32
|
+
def escape_for_css_selector(class_name)
|
|
33
|
+
class_name.gsub("/", '\/')
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -6,6 +6,10 @@ require "rails/railtie"
|
|
|
6
6
|
module ViewComponent
|
|
7
7
|
module ScopedStyles
|
|
8
8
|
class Railtie < Rails::Railtie
|
|
9
|
+
initializer "view_component.scoped_styles.component_generator_hook" do
|
|
10
|
+
require_relative "component_generator_hook"
|
|
11
|
+
end
|
|
12
|
+
|
|
9
13
|
config.after_initialize do
|
|
10
14
|
next unless Rails.env.development?
|
|
11
15
|
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "scoped_styles/version"
|
|
4
4
|
require_relative "scoped_styles/configuration"
|
|
5
|
+
require_relative "scoped_styles/css_class_prefix"
|
|
5
6
|
require "active_support/concern"
|
|
7
|
+
require "active_support/core_ext/enumerable"
|
|
6
8
|
require_relative "scoped_styles/concern"
|
|
7
9
|
require_relative "scoped_styles/stylist"
|
|
8
10
|
require_relative "scoped_styles/railtie"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: view_component-scoped_styles
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chris Edwards
|
|
@@ -112,9 +112,13 @@ files:
|
|
|
112
112
|
- Rakefile
|
|
113
113
|
- lib/generators/view_component/scoped_styles/install_generator.rb
|
|
114
114
|
- lib/generators/view_component/scoped_styles/templates/view_component_scoped_styles.rb.tt
|
|
115
|
+
- lib/generators/view_component/stylesheet/stylesheet_generator.rb
|
|
116
|
+
- lib/generators/view_component/stylesheet/templates/component.css.tt
|
|
115
117
|
- lib/view_component/scoped_styles.rb
|
|
118
|
+
- lib/view_component/scoped_styles/component_generator_hook.rb
|
|
116
119
|
- lib/view_component/scoped_styles/concern.rb
|
|
117
120
|
- lib/view_component/scoped_styles/configuration.rb
|
|
121
|
+
- lib/view_component/scoped_styles/css_class_prefix.rb
|
|
118
122
|
- lib/view_component/scoped_styles/railtie.rb
|
|
119
123
|
- lib/view_component/scoped_styles/stylist.rb
|
|
120
124
|
- lib/view_component/scoped_styles/stylist/writer.rb
|
|
@@ -141,7 +145,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
141
145
|
- !ruby/object:Gem::Version
|
|
142
146
|
version: '0'
|
|
143
147
|
requirements: []
|
|
144
|
-
rubygems_version: 4.0.
|
|
148
|
+
rubygems_version: 4.0.10
|
|
145
149
|
specification_version: 4
|
|
146
150
|
summary: Scoped, colocated CSS for ViewComponent components.
|
|
147
151
|
test_files: []
|