partials_fx 0.0.1 → 0.5.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/Gemfile +12 -0
- data/Gemfile.lock +243 -0
- data/README.md +146 -0
- data/Rakefile +10 -0
- data/bin/console +11 -0
- data/bin/release +19 -0
- data/bin/setup +8 -0
- data/lib/generators/USAGE +9 -0
- data/lib/generators/component_generator.rb +21 -0
- data/lib/generators/templates/component.html.erb.tt +4 -0
- data/lib/generators/templates/component.rb.tt +10 -0
- data/lib/partials_fx/component/attributes.rb +35 -0
- data/lib/partials_fx/component/render.rb +27 -0
- data/lib/partials_fx/component/slots.rb +21 -0
- data/lib/partials_fx/component/styles.rb +63 -0
- data/lib/partials_fx/component.rb +77 -0
- data/lib/partials_fx/configuration.rb +21 -0
- data/lib/partials_fx/errors.rb +11 -0
- data/lib/partials_fx/railtie.rb +37 -0
- data/lib/partials_fx/stylist/writer.rb +92 -0
- data/lib/partials_fx/stylist.rb +18 -0
- data/lib/partials_fx/version.rb +1 -1
- data/lib/partials_fx.rb +8 -0
- data/partials_fx.gemspec +24 -0
- metadata +55 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 76058417759f42122463669fea7eff9aae7871f62de129df795641bb592ecaff
|
4
|
+
data.tar.gz: 8673f99b6704a51ce92a3bcf7f108c51988695cf77a0b5495310071fa4a6f33e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b23685132aa916fc0ff3632ea3990c05dc2d3bf01e08a14a205674c2a56f26a6834a3b834ec13ab5e56dd84d69dec1a3584e380e947a95d148359360c8d1e976
|
7
|
+
data.tar.gz: 20a5b2495d7702b3c27c7a0fe770e70d9cb5dba976333542c27cc628608432f5ce7c9ec2db0637f2ba6e6bc6d5f0b34dd18fd6db06837de963f9c760e297619e
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,243 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
partials_fx (0.5.0)
|
5
|
+
rails (>= 7.2, < 8.1)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
actioncable (8.0.2)
|
11
|
+
actionpack (= 8.0.2)
|
12
|
+
activesupport (= 8.0.2)
|
13
|
+
nio4r (~> 2.0)
|
14
|
+
websocket-driver (>= 0.6.1)
|
15
|
+
zeitwerk (~> 2.6)
|
16
|
+
actionmailbox (8.0.2)
|
17
|
+
actionpack (= 8.0.2)
|
18
|
+
activejob (= 8.0.2)
|
19
|
+
activerecord (= 8.0.2)
|
20
|
+
activestorage (= 8.0.2)
|
21
|
+
activesupport (= 8.0.2)
|
22
|
+
mail (>= 2.8.0)
|
23
|
+
actionmailer (8.0.2)
|
24
|
+
actionpack (= 8.0.2)
|
25
|
+
actionview (= 8.0.2)
|
26
|
+
activejob (= 8.0.2)
|
27
|
+
activesupport (= 8.0.2)
|
28
|
+
mail (>= 2.8.0)
|
29
|
+
rails-dom-testing (~> 2.2)
|
30
|
+
actionpack (8.0.2)
|
31
|
+
actionview (= 8.0.2)
|
32
|
+
activesupport (= 8.0.2)
|
33
|
+
nokogiri (>= 1.8.5)
|
34
|
+
rack (>= 2.2.4)
|
35
|
+
rack-session (>= 1.0.1)
|
36
|
+
rack-test (>= 0.6.3)
|
37
|
+
rails-dom-testing (~> 2.2)
|
38
|
+
rails-html-sanitizer (~> 1.6)
|
39
|
+
useragent (~> 0.16)
|
40
|
+
actiontext (8.0.2)
|
41
|
+
actionpack (= 8.0.2)
|
42
|
+
activerecord (= 8.0.2)
|
43
|
+
activestorage (= 8.0.2)
|
44
|
+
activesupport (= 8.0.2)
|
45
|
+
globalid (>= 0.6.0)
|
46
|
+
nokogiri (>= 1.8.5)
|
47
|
+
actionview (8.0.2)
|
48
|
+
activesupport (= 8.0.2)
|
49
|
+
builder (~> 3.1)
|
50
|
+
erubi (~> 1.11)
|
51
|
+
rails-dom-testing (~> 2.2)
|
52
|
+
rails-html-sanitizer (~> 1.6)
|
53
|
+
activejob (8.0.2)
|
54
|
+
activesupport (= 8.0.2)
|
55
|
+
globalid (>= 0.3.6)
|
56
|
+
activemodel (8.0.2)
|
57
|
+
activesupport (= 8.0.2)
|
58
|
+
activerecord (8.0.2)
|
59
|
+
activemodel (= 8.0.2)
|
60
|
+
activesupport (= 8.0.2)
|
61
|
+
timeout (>= 0.4.0)
|
62
|
+
activestorage (8.0.2)
|
63
|
+
actionpack (= 8.0.2)
|
64
|
+
activejob (= 8.0.2)
|
65
|
+
activerecord (= 8.0.2)
|
66
|
+
activesupport (= 8.0.2)
|
67
|
+
marcel (~> 1.0)
|
68
|
+
activesupport (8.0.2)
|
69
|
+
base64
|
70
|
+
benchmark (>= 0.3)
|
71
|
+
bigdecimal
|
72
|
+
concurrent-ruby (~> 1.0, >= 1.3.1)
|
73
|
+
connection_pool (>= 2.2.5)
|
74
|
+
drb
|
75
|
+
i18n (>= 1.6, < 2)
|
76
|
+
logger (>= 1.4.2)
|
77
|
+
minitest (>= 5.1)
|
78
|
+
securerandom (>= 0.3)
|
79
|
+
tzinfo (~> 2.0, >= 2.0.5)
|
80
|
+
uri (>= 0.13.1)
|
81
|
+
ast (2.4.3)
|
82
|
+
base64 (0.2.0)
|
83
|
+
benchmark (0.4.0)
|
84
|
+
bigdecimal (3.1.9)
|
85
|
+
builder (3.3.0)
|
86
|
+
concurrent-ruby (1.3.5)
|
87
|
+
connection_pool (2.5.3)
|
88
|
+
crass (1.0.6)
|
89
|
+
date (3.4.1)
|
90
|
+
drb (2.2.1)
|
91
|
+
erubi (1.13.1)
|
92
|
+
globalid (1.2.1)
|
93
|
+
activesupport (>= 6.1)
|
94
|
+
i18n (1.14.7)
|
95
|
+
concurrent-ruby (~> 1.0)
|
96
|
+
io-console (0.8.0)
|
97
|
+
irb (1.15.2)
|
98
|
+
pp (>= 0.6.0)
|
99
|
+
rdoc (>= 4.0.0)
|
100
|
+
reline (>= 0.4.2)
|
101
|
+
json (2.11.3)
|
102
|
+
language_server-protocol (3.17.0.4)
|
103
|
+
lint_roller (1.1.0)
|
104
|
+
logger (1.7.0)
|
105
|
+
loofah (2.24.1)
|
106
|
+
crass (~> 1.0.2)
|
107
|
+
nokogiri (>= 1.12.0)
|
108
|
+
mail (2.8.1)
|
109
|
+
mini_mime (>= 0.1.1)
|
110
|
+
net-imap
|
111
|
+
net-pop
|
112
|
+
net-smtp
|
113
|
+
marcel (1.0.4)
|
114
|
+
mini_mime (1.1.5)
|
115
|
+
minitest (5.25.5)
|
116
|
+
net-imap (0.5.8)
|
117
|
+
date
|
118
|
+
net-protocol
|
119
|
+
net-pop (0.1.2)
|
120
|
+
net-protocol
|
121
|
+
net-protocol (0.2.2)
|
122
|
+
timeout
|
123
|
+
net-smtp (0.5.1)
|
124
|
+
net-protocol
|
125
|
+
nio4r (2.7.4)
|
126
|
+
nokogiri (1.18.8-arm64-darwin)
|
127
|
+
racc (~> 1.4)
|
128
|
+
parallel (1.27.0)
|
129
|
+
parser (3.3.8.0)
|
130
|
+
ast (~> 2.4.1)
|
131
|
+
racc
|
132
|
+
pp (0.6.2)
|
133
|
+
prettyprint
|
134
|
+
prettyprint (0.2.0)
|
135
|
+
prism (1.4.0)
|
136
|
+
psych (5.2.3)
|
137
|
+
date
|
138
|
+
stringio
|
139
|
+
racc (1.8.1)
|
140
|
+
rack (3.1.14)
|
141
|
+
rack-session (2.1.1)
|
142
|
+
base64 (>= 0.1.0)
|
143
|
+
rack (>= 3.0.0)
|
144
|
+
rack-test (2.2.0)
|
145
|
+
rack (>= 1.3)
|
146
|
+
rackup (2.2.1)
|
147
|
+
rack (>= 3)
|
148
|
+
rails (8.0.2)
|
149
|
+
actioncable (= 8.0.2)
|
150
|
+
actionmailbox (= 8.0.2)
|
151
|
+
actionmailer (= 8.0.2)
|
152
|
+
actionpack (= 8.0.2)
|
153
|
+
actiontext (= 8.0.2)
|
154
|
+
actionview (= 8.0.2)
|
155
|
+
activejob (= 8.0.2)
|
156
|
+
activemodel (= 8.0.2)
|
157
|
+
activerecord (= 8.0.2)
|
158
|
+
activestorage (= 8.0.2)
|
159
|
+
activesupport (= 8.0.2)
|
160
|
+
bundler (>= 1.15.0)
|
161
|
+
railties (= 8.0.2)
|
162
|
+
rails-dom-testing (2.2.0)
|
163
|
+
activesupport (>= 5.0.0)
|
164
|
+
minitest
|
165
|
+
nokogiri (>= 1.6)
|
166
|
+
rails-html-sanitizer (1.6.2)
|
167
|
+
loofah (~> 2.21)
|
168
|
+
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
169
|
+
railties (8.0.2)
|
170
|
+
actionpack (= 8.0.2)
|
171
|
+
activesupport (= 8.0.2)
|
172
|
+
irb (~> 1.13)
|
173
|
+
rackup (>= 1.0.0)
|
174
|
+
rake (>= 12.2)
|
175
|
+
thor (~> 1.0, >= 1.2.2)
|
176
|
+
zeitwerk (~> 2.6)
|
177
|
+
rainbow (3.1.1)
|
178
|
+
rake (13.2.1)
|
179
|
+
rdoc (6.13.1)
|
180
|
+
psych (>= 4.0.0)
|
181
|
+
regexp_parser (2.10.0)
|
182
|
+
reline (0.6.1)
|
183
|
+
io-console (~> 0.5)
|
184
|
+
rubocop (1.75.8)
|
185
|
+
json (~> 2.3)
|
186
|
+
language_server-protocol (~> 3.17.0.2)
|
187
|
+
lint_roller (~> 1.1.0)
|
188
|
+
parallel (~> 1.10)
|
189
|
+
parser (>= 3.3.0.2)
|
190
|
+
rainbow (>= 2.2.2, < 4.0)
|
191
|
+
regexp_parser (>= 2.9.3, < 3.0)
|
192
|
+
rubocop-ast (>= 1.44.0, < 2.0)
|
193
|
+
ruby-progressbar (~> 1.7)
|
194
|
+
unicode-display_width (>= 2.4.0, < 4.0)
|
195
|
+
rubocop-ast (1.44.1)
|
196
|
+
parser (>= 3.3.7.2)
|
197
|
+
prism (~> 1.4)
|
198
|
+
rubocop-performance (1.25.0)
|
199
|
+
lint_roller (~> 1.1)
|
200
|
+
rubocop (>= 1.75.0, < 2.0)
|
201
|
+
rubocop-ast (>= 1.38.0, < 2.0)
|
202
|
+
ruby-progressbar (1.13.0)
|
203
|
+
securerandom (0.4.1)
|
204
|
+
standard (1.50.0)
|
205
|
+
language_server-protocol (~> 3.17.0.2)
|
206
|
+
lint_roller (~> 1.0)
|
207
|
+
rubocop (~> 1.75.5)
|
208
|
+
standard-custom (~> 1.0.0)
|
209
|
+
standard-performance (~> 1.8)
|
210
|
+
standard-custom (1.0.2)
|
211
|
+
lint_roller (~> 1.0)
|
212
|
+
rubocop (~> 1.50)
|
213
|
+
standard-performance (1.8.0)
|
214
|
+
lint_roller (~> 1.1)
|
215
|
+
rubocop-performance (~> 1.25.0)
|
216
|
+
stringio (3.1.7)
|
217
|
+
thor (1.3.2)
|
218
|
+
timeout (0.4.3)
|
219
|
+
tzinfo (2.0.6)
|
220
|
+
concurrent-ruby (~> 1.0)
|
221
|
+
unicode-display_width (3.1.4)
|
222
|
+
unicode-emoji (~> 4.0, >= 4.0.4)
|
223
|
+
unicode-emoji (4.0.4)
|
224
|
+
uri (1.0.3)
|
225
|
+
useragent (0.16.11)
|
226
|
+
websocket-driver (0.7.7)
|
227
|
+
base64
|
228
|
+
websocket-extensions (>= 0.1.0)
|
229
|
+
websocket-extensions (0.1.5)
|
230
|
+
zeitwerk (2.7.2)
|
231
|
+
|
232
|
+
PLATFORMS
|
233
|
+
arm64-darwin-23
|
234
|
+
|
235
|
+
DEPENDENCIES
|
236
|
+
irb
|
237
|
+
minitest (~> 5.25, >= 5.25.5)
|
238
|
+
partials_fx!
|
239
|
+
rake (~> 13.0)
|
240
|
+
standard (~> 1.50)
|
241
|
+
|
242
|
+
BUNDLED WITH
|
243
|
+
2.6.8
|
data/README.md
ADDED
@@ -0,0 +1,146 @@
|
|
1
|
+
# Partials Fx
|
2
|
+
|
3
|
+
Partials FX extends Rails' partials to make them quack more like components. Each partial, located in `app/views/components/`, is backed by a Ruby class with the same name. It also supports CSS modules, meaning you define CSS in the component class and it gets “scoped” to that component only. It is powering the [self-hosted Community Software, Forge](https://forge.railsdesigner.com/).
|
4
|
+
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
```bash
|
9
|
+
bundle add partials_fx
|
10
|
+
```
|
11
|
+
|
12
|
+
|
13
|
+
## Usage
|
14
|
+
|
15
|
+
`bin/rails generate component Avatar`
|
16
|
+
|
17
|
+
This creates two files:
|
18
|
+
- `app/views/components/_avatar_component.html.erb`;
|
19
|
+
- `app/views/components/avatar_component.rb`.
|
20
|
+
|
21
|
+
They could look like this:
|
22
|
+
```erb
|
23
|
+
<span id="user-avatar" class="<%= component_class %> <%= avatar_size %>">
|
24
|
+
<% if avatar.attached? %>
|
25
|
+
<%= image_tag avatar.variant(avatar_variant), alt: alt_text %>
|
26
|
+
<% else %>
|
27
|
+
<%= tag.span name.first.upcase, class: "initial" %>
|
28
|
+
<% end %>
|
29
|
+
</span>
|
30
|
+
```
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
# app/views/components/avatar_component.rb
|
34
|
+
class AvatarComponent < PartialsFx::Component
|
35
|
+
attribute :user, required: true
|
36
|
+
attribute :size, :string, default: "md", inquiry: true
|
37
|
+
attribute :variant, :string, default: "thumb", inquiry: true
|
38
|
+
|
39
|
+
def avatar_size
|
40
|
+
return "avatar-profile" if variant.profile?
|
41
|
+
|
42
|
+
class_names(
|
43
|
+
"avatar-xs": size.xs?,
|
44
|
+
"avatar-sm": size.sm?,
|
45
|
+
"avatar-md": size.md?,
|
46
|
+
"avatar-lg": size.lg?,
|
47
|
+
"avatar-xl": size.xl?
|
48
|
+
)
|
49
|
+
end
|
50
|
+
|
51
|
+
def name
|
52
|
+
@name ||= user.decorate.name
|
53
|
+
end
|
54
|
+
|
55
|
+
def avatar = profile.avatar
|
56
|
+
|
57
|
+
def alt_text = "Avatar for #{name}"
|
58
|
+
|
59
|
+
def avatar_variant
|
60
|
+
variant.thumb? ? :thumb : :profile
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def profile
|
66
|
+
@profile ||= user.profile
|
67
|
+
end
|
68
|
+
end
|
69
|
+
```
|
70
|
+
|
71
|
+
In views:
|
72
|
+
```erb
|
73
|
+
<%= render AvatarComponent.new user: Current.user %>
|
74
|
+
```
|
75
|
+
|
76
|
+
|
77
|
+
## CSS modules
|
78
|
+
|
79
|
+
A powerful feature of Partials FX is support for “CSS modules”. Meaning that styles are locally scoped by default, preventing style conflicts across components. This allows the use of the same class names in different components without worrying about collisions, as Partials FX automatically generates unique class names, for the parent element, during compilation. CSS modules improve maintainability by creating a clear connection between components and their styles, making it easier to manage complex UI systems.
|
80
|
+
|
81
|
+
It works like this. First define a `styles` block in the class:
|
82
|
+
```ruby
|
83
|
+
class AvatarComponent < PartialsFx::Component
|
84
|
+
# …
|
85
|
+
|
86
|
+
styles do
|
87
|
+
<<~CSS
|
88
|
+
.component {
|
89
|
+
}
|
90
|
+
CSS
|
91
|
+
end
|
92
|
+
|
93
|
+
# …
|
94
|
+
end
|
95
|
+
```
|
96
|
+
|
97
|
+
Then add the `component_class` method to the the component partial:
|
98
|
+
```erb
|
99
|
+
<span id="user-avatar" class="<%= component_class %> <%= avatar_size %>">
|
100
|
+
</span>
|
101
|
+
```
|
102
|
+
|
103
|
+
You can now add any CSS selector in the `.component` selector, which itself will compile to something like:
|
104
|
+
```css
|
105
|
+
/* app/assets/stylesheets/components.partialsfx.css */
|
106
|
+
.c-9af2b949 {
|
107
|
+
--avatar-font-size-fallback:
|
108
|
+
clamp(.75rem, calc(var(--avatar-height) * .8), 2rem);
|
109
|
+
display: inline flex;
|
110
|
+
align-items: center;
|
111
|
+
justify-content: center;
|
112
|
+
height: var(--avatar-height, 1rem);
|
113
|
+
/* … */
|
114
|
+
|
115
|
+
.initial {
|
116
|
+
font-size: var(--avatar-font-size, var(--avatar-font-size-fallback));
|
117
|
+
font-weight: 600;
|
118
|
+
color: var(--avatar-color, var(--base-60));
|
119
|
+
}
|
120
|
+
|
121
|
+
&.avatar-profile {}
|
122
|
+
|
123
|
+
/* … */
|
124
|
+
}
|
125
|
+
```
|
126
|
+
|
127
|
+
Inside the component partial, `component_class` will be replaced with the unique selector `.c-9af2b949`. Because [nested selectors are available in CSS](https://developer.mozilla.org/en-US/docs/Web/CSS/Nesting_selector) this will work smoothly. Keeping your selectors scoped to the one component. Making your app's CSS way more maintainable.
|
128
|
+
|
129
|
+
Make sure to import the automatically-created CSS file from Partials FX into your `application.css`, e.g.
|
130
|
+
```css
|
131
|
+
@layer reset, components, utilities;
|
132
|
+
|
133
|
+
/* … */
|
134
|
+
@import url("./components.partialsfx.css") layer(components);
|
135
|
+
/* … */
|
136
|
+
```
|
137
|
+
|
138
|
+
|
139
|
+
# Contributing
|
140
|
+
|
141
|
+
This project uses [Standard](https://github.com/testdouble/standard) for formatting Ruby code. Please make sure to run `be standardrb` before submitting pull requests. Run tests via `rails test`.
|
142
|
+
|
143
|
+
|
144
|
+
## License
|
145
|
+
|
146
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "partials_fx"
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
require "irb"
|
11
|
+
IRB.start(__FILE__)
|
data/bin/release
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
#!/usr/bin/env bash
|
2
|
+
|
3
|
+
VERSION=$1
|
4
|
+
|
5
|
+
if [ -z "$VERSION" ]; then
|
6
|
+
echo "Error: The version number is required."
|
7
|
+
echo "Usage: $0 <version-number>"
|
8
|
+
exit 1
|
9
|
+
fi
|
10
|
+
|
11
|
+
printf "module PartialsFx\n VERSION = \"$VERSION\"\nend\n" > ./lib/partials_fx/version.rb
|
12
|
+
bundle
|
13
|
+
git add Gemfile.lock lib/partials_fx/version.rb
|
14
|
+
git commit -m "Bump version for $VERSION"
|
15
|
+
git push
|
16
|
+
git tag v$VERSION
|
17
|
+
git push --tags
|
18
|
+
gem build partials_fx.gemspec
|
19
|
+
gem push "partials_fx-$VERSION.gem"
|
data/bin/setup
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/generators"
|
4
|
+
require "rails/generators/named_base"
|
5
|
+
|
6
|
+
class ComponentGenerator < Rails::Generators::NamedBase
|
7
|
+
source_root File.expand_path("templates", __dir__)
|
8
|
+
|
9
|
+
check_class_collision suffix: "Component"
|
10
|
+
|
11
|
+
class_option :component_styles, type: :boolean, default: PartialsFx.configuration.enable_component_styles
|
12
|
+
|
13
|
+
def copy_component_file
|
14
|
+
template "component.rb", File.join(PartialsFx.configuration.components_path, class_path, "#{file_name}_component.rb")
|
15
|
+
template "component.html.erb", File.join(PartialsFx.configuration.components_path, class_path, "_#{file_name}_component.html.erb")
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def parent_class = defined?(ApplicationComponent) ? ApplicationComponent : PartialsFx::Component
|
21
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module PartialsFx
|
2
|
+
class Component
|
3
|
+
module Attributes
|
4
|
+
module ClassMethods
|
5
|
+
def attribute(name, *arguments, required: false, inquiry: false, **options)
|
6
|
+
track_required_attribute(name) if required
|
7
|
+
|
8
|
+
super(name, *arguments, **options)
|
9
|
+
|
10
|
+
return unless inquiry
|
11
|
+
|
12
|
+
define_method(name) do
|
13
|
+
value = super()
|
14
|
+
|
15
|
+
return value if value.nil?
|
16
|
+
|
17
|
+
ActiveSupport::StringInquirer.new(value.to_s)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def required_attributes
|
22
|
+
@required_attributes || []
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def track_required_attribute(attribute_name)
|
28
|
+
@required_attributes ||= []
|
29
|
+
|
30
|
+
@required_attributes << attribute_name
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module PartialsFx
|
2
|
+
class Component
|
3
|
+
module Render
|
4
|
+
def render_in(view_context, &block)
|
5
|
+
@view_context = view_context
|
6
|
+
|
7
|
+
capture_content_if_block_given(&block)
|
8
|
+
|
9
|
+
partial_path = build_partial_path
|
10
|
+
|
11
|
+
view_context.render(partial: partial_path, locals: to_locals)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def capture_content_if_block_given(&block)
|
17
|
+
return unless block_given?
|
18
|
+
|
19
|
+
@content = @view_context.capture do
|
20
|
+
yield self
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def build_partial_path = "components/#{self.class.name.underscore}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module PartialsFx
|
2
|
+
class Component
|
3
|
+
module Slots
|
4
|
+
module ClassMethods
|
5
|
+
def slot(slot_name, default: nil)
|
6
|
+
define_method(slot_name) do |&block|
|
7
|
+
@slots ||= {}
|
8
|
+
|
9
|
+
if block
|
10
|
+
@slots[slot_name] = view_context.capture(&block)
|
11
|
+
|
12
|
+
nil # Prevent output during capture
|
13
|
+
else
|
14
|
+
@slots[slot_name] || (default.is_a?(Symbol) ? send(default) : default)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module PartialsFx
|
2
|
+
class Component
|
3
|
+
module Styles
|
4
|
+
module ClassMethods
|
5
|
+
def styles(&block)
|
6
|
+
@styles_block = block
|
7
|
+
|
8
|
+
register_styles_if_rails_loaded
|
9
|
+
end
|
10
|
+
|
11
|
+
def component_styles
|
12
|
+
return @component_styles if defined?(@component_styles)
|
13
|
+
return nil unless @styles_block
|
14
|
+
|
15
|
+
generate_component_styles
|
16
|
+
end
|
17
|
+
|
18
|
+
def register_styles
|
19
|
+
return unless @styles_block
|
20
|
+
|
21
|
+
Stylist.register(self)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def register_styles_if_rails_loaded
|
27
|
+
register_styles if defined?(Rails) && Rails.root
|
28
|
+
end
|
29
|
+
|
30
|
+
def generate_component_styles
|
31
|
+
styles_content = @styles_block.call
|
32
|
+
|
33
|
+
@component_id = generate_component_id(styles_content)
|
34
|
+
@component_styles = process_styles_with_component_id(styles_content)
|
35
|
+
end
|
36
|
+
|
37
|
+
def generate_component_id(styles_content)
|
38
|
+
hash = Digest::MD5.hexdigest(styles_content)[0..7]
|
39
|
+
|
40
|
+
"c-#{hash}"
|
41
|
+
end
|
42
|
+
|
43
|
+
def process_styles_with_component_id(styles_content)
|
44
|
+
styles_content.gsub(/\.component\b/, ".#{@component_id}")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def component_class
|
49
|
+
return nil unless component_has_styles?
|
50
|
+
|
51
|
+
self.class.component_styles
|
52
|
+
self.class.instance_variable_get(:@component_id)
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def component_has_styles?
|
58
|
+
self.class.instance_variable_defined?(:@styles_block) &&
|
59
|
+
self.class.instance_variable_get(:@styles_block)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_model"
|
4
|
+
require "action_view"
|
5
|
+
|
6
|
+
require "partials_fx/component/attributes"
|
7
|
+
require "partials_fx/component/slots"
|
8
|
+
require "partials_fx/component/styles"
|
9
|
+
require "partials_fx/component/render"
|
10
|
+
|
11
|
+
module PartialsFx
|
12
|
+
class Component
|
13
|
+
include ActiveModel::Model
|
14
|
+
include ActionView::Helpers::TagHelper
|
15
|
+
include ActiveModel::Attributes
|
16
|
+
|
17
|
+
include Styles
|
18
|
+
include Render
|
19
|
+
|
20
|
+
extend Attributes::ClassMethods
|
21
|
+
extend Slots::ClassMethods
|
22
|
+
extend Styles::ClassMethods
|
23
|
+
|
24
|
+
attr_accessor :content, :view_context
|
25
|
+
|
26
|
+
class << self
|
27
|
+
def inherited(subclass)
|
28
|
+
super
|
29
|
+
|
30
|
+
subclass.instance_eval do
|
31
|
+
@pending_registration = true
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def initialize(**component_attributes)
|
37
|
+
validate_required_attributes(component_attributes)
|
38
|
+
|
39
|
+
super
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_locals
|
43
|
+
base_locals = attributes.transform_keys(&:to_sym)
|
44
|
+
|
45
|
+
base_locals
|
46
|
+
.merge(public_methods(false).to_h { [_1, public_send(_1)] })
|
47
|
+
.merge(slot_locals)
|
48
|
+
.merge(content: content, component_class: component_class)
|
49
|
+
end
|
50
|
+
|
51
|
+
def method_missing(method, *arguments, &block)
|
52
|
+
if respond_to_missing?(method)
|
53
|
+
public_send(method, *arguments, &block)
|
54
|
+
else
|
55
|
+
super
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def respond_to_missing?(method, include_private = false)
|
60
|
+
public_methods.include?(method) || super
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def validate_required_attributes(component_attributes)
|
66
|
+
self.class.required_attributes.each do |required_attribute|
|
67
|
+
unless component_attributes.key?(required_attribute)
|
68
|
+
raise MissingRequiredAttributeError.new(required_attribute)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def slot_locals
|
74
|
+
@slots || {}
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PartialsFx
|
4
|
+
class Configuration
|
5
|
+
attr_accessor :components_path, :enable_component_styles, :components_layer
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@components_path = File.join("app", "views", "components")
|
9
|
+
@enable_component_styles = true
|
10
|
+
@components_layer = nil # e.g. "components"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def configuration
|
16
|
+
@configuration ||= Configuration.new
|
17
|
+
end
|
18
|
+
|
19
|
+
def configure = yield(configuration)
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/railtie"
|
4
|
+
|
5
|
+
module PartialsFx
|
6
|
+
class Railtie < Rails::Railtie
|
7
|
+
initializer "partials_fx.autoload", before: :set_autoload_paths do |app|
|
8
|
+
app.config.autoload_paths << app.root.join(PartialsFx.configuration.components_path)
|
9
|
+
end
|
10
|
+
|
11
|
+
config.after_initialize do
|
12
|
+
PartialsFx::Railtie.load_and_register_components if PartialsFx.configuration.enable_component_styles
|
13
|
+
end
|
14
|
+
|
15
|
+
if Rails.env.development?
|
16
|
+
config.to_prepare do
|
17
|
+
PartialsFx::Railtie.register_all_components if PartialsFx.configuration.enable_component_styles
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class << self
|
22
|
+
def components_glob = Rails.root.join(PartialsFx.configuration.components_path, "**/*_component.rb")
|
23
|
+
|
24
|
+
def load_and_register_components
|
25
|
+
Dir[components_glob].each { require_dependency _1 }
|
26
|
+
|
27
|
+
register_all_components
|
28
|
+
end
|
29
|
+
|
30
|
+
def register_all_components
|
31
|
+
ObjectSpace.each_object(Class)
|
32
|
+
.select { _1 < PartialsFx::Component }
|
33
|
+
.each(&:register_styles)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PartialsFx
|
4
|
+
class Stylist
|
5
|
+
class Writer
|
6
|
+
def self.print(component_class)
|
7
|
+
new.print!(component_class)
|
8
|
+
end
|
9
|
+
|
10
|
+
def print!(component_class)
|
11
|
+
component_name = component_class.name
|
12
|
+
component_class.instance_variable_get(:@component_id)
|
13
|
+
styles = component_class.component_styles
|
14
|
+
|
15
|
+
ensure_stylesheet_exists!
|
16
|
+
|
17
|
+
update_stylesheet_with(component_name, styles)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def ensure_stylesheet_exists!
|
23
|
+
return if File.exist?(stylesheet_path)
|
24
|
+
|
25
|
+
FileUtils.mkdir_p(File.dirname(stylesheet_path))
|
26
|
+
File.write(stylesheet_path, initial_content)
|
27
|
+
end
|
28
|
+
|
29
|
+
def update_stylesheet_with(component_name, styles)
|
30
|
+
content = File.read(stylesheet_path)
|
31
|
+
updated_content = merge_styles_into(content, component_name, styles)
|
32
|
+
|
33
|
+
write_atomically(updated_content)
|
34
|
+
|
35
|
+
notify_asset_pipeline!
|
36
|
+
end
|
37
|
+
|
38
|
+
def merge_styles_into(content, component_name, styles)
|
39
|
+
wrapped_styles = wrap_with(component_name, styles)
|
40
|
+
pattern = style_pattern_for(component_name)
|
41
|
+
|
42
|
+
if content.match?(pattern)
|
43
|
+
content.gsub(pattern, wrapped_styles.strip)
|
44
|
+
else
|
45
|
+
append_styles_to(content, wrapped_styles)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def write_atomically(content)
|
50
|
+
if PartialsFx.configuration.components_layer
|
51
|
+
content = content.rstrip + "\n}\n"
|
52
|
+
end
|
53
|
+
|
54
|
+
temp_file = "#{stylesheet_path}.tmp"
|
55
|
+
|
56
|
+
File.write(temp_file, content)
|
57
|
+
FileUtils.mv(temp_file, stylesheet_path)
|
58
|
+
end
|
59
|
+
|
60
|
+
def notify_asset_pipeline! = FileUtils.touch(stylesheet_path)
|
61
|
+
|
62
|
+
def wrap_with(component_name, styles)
|
63
|
+
start_marker = "/* #{component_name} */\n"
|
64
|
+
end_marker = "/* /#{component_name} */"
|
65
|
+
|
66
|
+
"#{start_marker}#{styles}#{end_marker}"
|
67
|
+
end
|
68
|
+
|
69
|
+
def style_pattern_for(component_name)
|
70
|
+
/\/\* #{Regexp.escape(component_name)} \*\/\n.*?\/\* \/#{Regexp.escape(component_name)} \*\//m
|
71
|
+
end
|
72
|
+
|
73
|
+
def append_styles_to(content, wrapped_styles)
|
74
|
+
[content.chomp, wrapped_styles, "\n"].join("\n")
|
75
|
+
end
|
76
|
+
|
77
|
+
def initial_content
|
78
|
+
content = "/* Generated by PartialsFx - Do not edit manually */\n"
|
79
|
+
|
80
|
+
if (layer = PartialsFx.configuration.components_layer)
|
81
|
+
content += "@layer #{layer} {\n"
|
82
|
+
end
|
83
|
+
|
84
|
+
content
|
85
|
+
end
|
86
|
+
|
87
|
+
def stylesheet_path
|
88
|
+
@stylesheet_path ||= Rails.root.join("app/assets/stylesheets/components.partialsfx.css")
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "partials_fx/stylist/writer"
|
4
|
+
|
5
|
+
module PartialsFx
|
6
|
+
class Stylist
|
7
|
+
def self.register(component_class)
|
8
|
+
return if unstyled?(component_class)
|
9
|
+
|
10
|
+
Writer.print(component_class)
|
11
|
+
end
|
12
|
+
|
13
|
+
private_class_method def self.unstyled?(component_class)
|
14
|
+
!component_class.component_styles &&
|
15
|
+
!component_class.instance_variable_get(:@component_id)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/partials_fx/version.rb
CHANGED
data/lib/partials_fx.rb
CHANGED
@@ -1,4 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "partials_fx/version"
|
4
|
+
require "partials_fx/configuration"
|
5
|
+
require "generators/component_generator"
|
6
|
+
require "partials_fx/errors"
|
7
|
+
require "partials_fx/component"
|
8
|
+
require "partials_fx/stylist"
|
9
|
+
require "partials_fx/railtie"
|
2
10
|
|
3
11
|
module PartialsFx
|
4
12
|
end
|
data/partials_fx.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/partials_fx/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "partials_fx"
|
7
|
+
spec.version = PartialsFx::VERSION
|
8
|
+
spec.authors = ["Rails Designer"]
|
9
|
+
spec.email = ["devs@railsdesigner.com"]
|
10
|
+
|
11
|
+
spec.summary = "Extends Rails' partials to make them quack more like components"
|
12
|
+
spec.description = "Partials FX extends Rails' partials to make them quack more like components. Each partial, located in app/views/components/, is backed by a Ruby class with the same name. It also supports CSS modules, meaning you define CSS in the component class and it gets “scoped” to that component only."
|
13
|
+
spec.homepage = "https://railsdesigner.com/partials-fx/"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
17
|
+
spec.metadata["source_code_uri"] = "https://github.com/Rails-Designer/partials_fx/"
|
18
|
+
|
19
|
+
spec.files = Dir["{bin,lib}/**/*", "Rakefile", "README.md", "partials_fx.gemspec", "Gemfile", "Gemfile.lock"]
|
20
|
+
|
21
|
+
spec.required_ruby_version = ">= 3.4.0"
|
22
|
+
|
23
|
+
spec.add_dependency "rails", ">= 7.2", "< 8.1"
|
24
|
+
end
|
metadata
CHANGED
@@ -1,26 +1,74 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: partials_fx
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rails Designer
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
-
dependencies:
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: rails
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - ">="
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '7.2'
|
19
|
+
- - "<"
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '8.1'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
version: '7.2'
|
29
|
+
- - "<"
|
30
|
+
- !ruby/object:Gem::Version
|
31
|
+
version: '8.1'
|
32
|
+
description: Partials FX extends Rails' partials to make them quack more like components.
|
33
|
+
Each partial, located in app/views/components/, is backed by a Ruby class with the
|
34
|
+
same name. It also supports CSS modules, meaning you define CSS in the component
|
35
|
+
class and it gets “scoped” to that component only.
|
12
36
|
email:
|
13
37
|
- devs@railsdesigner.com
|
14
38
|
executables: []
|
15
39
|
extensions: []
|
16
40
|
extra_rdoc_files: []
|
17
41
|
files:
|
42
|
+
- Gemfile
|
43
|
+
- Gemfile.lock
|
44
|
+
- README.md
|
45
|
+
- Rakefile
|
46
|
+
- bin/console
|
47
|
+
- bin/release
|
48
|
+
- bin/setup
|
49
|
+
- lib/generators/USAGE
|
50
|
+
- lib/generators/component_generator.rb
|
51
|
+
- lib/generators/templates/component.html.erb.tt
|
52
|
+
- lib/generators/templates/component.rb.tt
|
18
53
|
- lib/partials_fx.rb
|
54
|
+
- lib/partials_fx/component.rb
|
55
|
+
- lib/partials_fx/component/attributes.rb
|
56
|
+
- lib/partials_fx/component/render.rb
|
57
|
+
- lib/partials_fx/component/slots.rb
|
58
|
+
- lib/partials_fx/component/styles.rb
|
59
|
+
- lib/partials_fx/configuration.rb
|
60
|
+
- lib/partials_fx/errors.rb
|
61
|
+
- lib/partials_fx/railtie.rb
|
62
|
+
- lib/partials_fx/stylist.rb
|
63
|
+
- lib/partials_fx/stylist/writer.rb
|
19
64
|
- lib/partials_fx/version.rb
|
20
|
-
|
65
|
+
- partials_fx.gemspec
|
66
|
+
homepage: https://railsdesigner.com/partials-fx/
|
21
67
|
licenses:
|
22
68
|
- MIT
|
23
|
-
metadata:
|
69
|
+
metadata:
|
70
|
+
homepage_uri: https://railsdesigner.com/partials-fx/
|
71
|
+
source_code_uri: https://github.com/Rails-Designer/partials_fx/
|
24
72
|
rdoc_options: []
|
25
73
|
require_paths:
|
26
74
|
- lib
|
@@ -28,14 +76,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
28
76
|
requirements:
|
29
77
|
- - ">="
|
30
78
|
- !ruby/object:Gem::Version
|
31
|
-
version: 3.
|
79
|
+
version: 3.4.0
|
32
80
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
33
81
|
requirements:
|
34
82
|
- - ">="
|
35
83
|
- !ruby/object:Gem::Version
|
36
84
|
version: '0'
|
37
85
|
requirements: []
|
38
|
-
rubygems_version: 3.6.
|
86
|
+
rubygems_version: 3.6.9
|
39
87
|
specification_version: 4
|
40
|
-
summary: Rails partials
|
88
|
+
summary: Extends Rails' partials to make them quack more like components
|
41
89
|
test_files: []
|