view_component-scoped_styles 0.5.0 → 0.6.1
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 +44 -1
- data/lib/generators/view_component/scoped_styles/templates/view_component_scoped_styles.rb.tt +6 -0
- 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 +15 -0
- data/lib/view_component/scoped_styles/concern.rb +76 -11
- 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 +1 -0
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 206f58f9f063b3311d866937c80bcd1a1566621102ebf7135d2aa373f89d32e4
|
|
4
|
+
data.tar.gz: f0ac0545c6cb63d6dd6739e9e4356ee6b0ba465025e39920345724afa2ce396e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fae7281bb414c6329bd19c5a58a4c0b23e0ac2eb11f24262ac34e6bbe278b7542de58b186f8838030eb3db9b439b0d618a4db12f4691cbe62ffe6b3daefe25f0
|
|
7
|
+
data.tar.gz: 0ff154a44d59306b1e9c793656b45696fd91be6715036ee3eebd62c1bb3f8c1fb988130f4877afe72c9630783a70dd8d55cc8b7d2e27f522817746d9e520316c
|
data/README.md
CHANGED
|
@@ -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,6 +178,32 @@ end
|
|
|
161
178
|
</div>
|
|
162
179
|
```
|
|
163
180
|
|
|
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
|
+
|
|
164
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`.
|
|
165
208
|
|
|
166
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):
|
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 %>
|
|
@@ -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,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "generators/view_component/component/component_generator"
|
|
5
|
+
|
|
6
|
+
module ViewComponent
|
|
7
|
+
module Generators
|
|
8
|
+
class ComponentGenerator
|
|
9
|
+
class_option :stylesheet, type: :boolean,
|
|
10
|
+
default: ViewComponent::Base.config.generate.stylesheet
|
|
11
|
+
|
|
12
|
+
hook_for :stylesheet, type: :boolean
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
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
|
|
@@ -115,13 +124,26 @@ module ViewComponent
|
|
|
115
124
|
end
|
|
116
125
|
|
|
117
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
|
|
118
136
|
styles_content = generate_styles_content
|
|
119
137
|
css_classes = extract_css_classes(styles_content)
|
|
120
138
|
primary_class = primary_css_class(css_classes)
|
|
121
139
|
|
|
122
140
|
@component_id = component_id_for(primary_class, styles_content)
|
|
123
141
|
@component_class_map = build_component_class_map(styles_content, css_classes, primary_class)
|
|
124
|
-
@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
|
|
125
147
|
end
|
|
126
148
|
|
|
127
149
|
def generate_styles_content
|
|
@@ -161,6 +183,17 @@ module ViewComponent
|
|
|
161
183
|
end
|
|
162
184
|
end
|
|
163
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)}"
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
164
197
|
def component_id_for(primary_class, styles_content)
|
|
165
198
|
if ignored_css_class?(primary_class)
|
|
166
199
|
primary_class
|
|
@@ -202,6 +235,36 @@ module ViewComponent
|
|
|
202
235
|
end
|
|
203
236
|
end
|
|
204
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
|
+
|
|
205
268
|
def clear_component_style_cache
|
|
206
269
|
CACHED_VARIABLES.each do |ivar|
|
|
207
270
|
remove_instance_variable(ivar) if instance_variable_defined?(ivar)
|
|
@@ -215,28 +278,30 @@ module ViewComponent
|
|
|
215
278
|
# appears in the CSS, otherwise the first class in the stylesheet).
|
|
216
279
|
#
|
|
217
280
|
# @param name [String, Symbol] CSS class without a leading dot (e.g. +"input-box"+)
|
|
218
|
-
|
|
219
|
-
|
|
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)
|
|
220
285
|
|
|
221
|
-
|
|
286
|
+
from.component_styles
|
|
222
287
|
|
|
223
288
|
if name
|
|
224
|
-
class_map =
|
|
289
|
+
class_map = from.instance_variable_get(:@component_class_map)
|
|
225
290
|
class_map[name.to_s.delete_prefix(".")]
|
|
226
291
|
else
|
|
227
|
-
|
|
292
|
+
from.instance_variable_get(:@component_id)
|
|
228
293
|
end
|
|
229
294
|
end
|
|
230
295
|
|
|
231
296
|
private
|
|
232
297
|
|
|
233
|
-
def component_has_stylesheet?
|
|
234
|
-
|
|
298
|
+
def component_has_stylesheet?(component_class = self.class)
|
|
299
|
+
component_class.has_stylesheet?
|
|
235
300
|
end
|
|
236
301
|
|
|
237
|
-
def component_has_styles?
|
|
238
|
-
|
|
239
|
-
|
|
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)
|
|
240
305
|
end
|
|
241
306
|
end
|
|
242
307
|
end
|
|
@@ -4,6 +4,7 @@ require_relative "scoped_styles/version"
|
|
|
4
4
|
require_relative "scoped_styles/configuration"
|
|
5
5
|
require_relative "scoped_styles/css_class_prefix"
|
|
6
6
|
require "active_support/concern"
|
|
7
|
+
require "active_support/core_ext/enumerable"
|
|
7
8
|
require_relative "scoped_styles/concern"
|
|
8
9
|
require_relative "scoped_styles/stylist"
|
|
9
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.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chris Edwards
|
|
@@ -112,7 +112,10 @@ 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
|
|
118
121
|
- lib/view_component/scoped_styles/css_class_prefix.rb
|
|
@@ -142,7 +145,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
142
145
|
- !ruby/object:Gem::Version
|
|
143
146
|
version: '0'
|
|
144
147
|
requirements: []
|
|
145
|
-
rubygems_version: 4.0.
|
|
148
|
+
rubygems_version: 4.0.10
|
|
146
149
|
specification_version: 4
|
|
147
150
|
summary: Scoped, colocated CSS for ViewComponent components.
|
|
148
151
|
test_files: []
|