protos 0.5.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +159 -58
- data/lib/protos/attributes.rb +1 -18
- data/lib/protos/component.rb +2 -2
- data/lib/protos/mix.rb +39 -0
- data/lib/protos/version.rb +1 -1
- data/lib/protos.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '0796561a80e75fb39f8b35ec75257104adc90b21227ef1525876576d10415269'
|
4
|
+
data.tar.gz: 394a8ee190d1d2647476db45d2d9b9edb156bc42a3935898d6f1deca35261326
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 59e2288d2ae8adfc3b352838dda25c776edddf48d99b310181629e78f09831552be23e36adedaca7bf3601678cb08950500f4b72f2ed609324ccd72ddd32a92d
|
7
|
+
data.tar.gz: e0e4beda7e4cbff4e3b669899b65df2ec7c37fe388e75b561418ce1cc65abd4cb1cfecabd743f964bf0dee98de1c4ccdbf9f7e76f77078f294a8bfb0a4272802
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,14 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.6.0] - 2024-09-04
|
4
|
+
|
5
|
+
- Changes how merging attributes works to only mix on certain attributes,
|
6
|
+
overriding on all others. This is opposite to how attributes used to be merged
|
7
|
+
by default. This is a fix for attributes like `value` where you actually need
|
8
|
+
to override them.
|
9
|
+
- Adds tests for all Rails components
|
10
|
+
- Adds a separate tested `Mix` class for handling attribute merging
|
11
|
+
|
3
12
|
## [0.5.0] - 2024-08-27
|
4
13
|
|
5
14
|
- Fixes all accessibility violations according to Axe Core
|
data/README.md
CHANGED
@@ -20,10 +20,90 @@ Other Phlex based UI libraries worth checking out:
|
|
20
20
|
- [PhlexUI](https://phlexui.com/)
|
21
21
|
- [ZestUI](https://zestui.com/)
|
22
22
|
|
23
|
+
Thinking of making your next static site using Phlex? Check out
|
24
|
+
[staticky](https://github.com/nolantait/staticky). The protos docs were
|
25
|
+
published using it.
|
26
|
+
|
27
|
+
## Phlex components
|
28
|
+
|
29
|
+
Phlex is a fantastic framework for building frontend components in pure Ruby:
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
class Navbar
|
33
|
+
def view_template
|
34
|
+
header(class: "flex items-center justify-between") do
|
35
|
+
h3 { "My site" }
|
36
|
+
button { "Log out" }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
```
|
41
|
+
|
42
|
+
But how can we sometimes render this `Navbar` with a different background color?
|
43
|
+
|
44
|
+
It would be nice to have our components take a class like any other element:
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
render Navbar.new(class: "bg-primary")
|
48
|
+
```
|
49
|
+
|
50
|
+
Unfortunately `class` is a special keyword in Ruby, so we need to do some
|
51
|
+
awkward handling to use it like this:
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
class Navbar
|
55
|
+
def initialize(**options)
|
56
|
+
# Keyword `class` is a special word in Ruby so we have to awkwardly unwrap
|
57
|
+
# like this instead of using keyword arguments
|
58
|
+
@classes = options[:class]
|
59
|
+
end
|
60
|
+
|
61
|
+
def view_template
|
62
|
+
header(class: "#{@classes} flex items-center justify-between") do
|
63
|
+
h3 { "My site" }
|
64
|
+
button { "Log out" }
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
```
|
69
|
+
|
70
|
+
Now we can pass in a style to our container, but what about overriding the style
|
71
|
+
of the `h3` tag?
|
72
|
+
|
73
|
+
```ruby
|
74
|
+
class Navbar
|
75
|
+
def initialize(**options)
|
76
|
+
# Keyword `class` is a special word in Ruby so we have to awkwardly unwrap
|
77
|
+
# like this instead of using keyword arguments
|
78
|
+
@container_classes = options[:class]
|
79
|
+
@title_classes = options[:title_class]
|
80
|
+
end
|
81
|
+
|
82
|
+
def view_template
|
83
|
+
header(class: "#{@classes} flex items-center justify-between") do
|
84
|
+
h3(class: @title_classes) { "My site" }
|
85
|
+
button { "Log out" }
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
```
|
90
|
+
|
91
|
+
Eventually everyone makes a kind of ad-hoc system for specifying styles. It gets
|
92
|
+
more complicated when you have attributes like a data-controller. How do you
|
93
|
+
give a good experience letting people using your components to add their own
|
94
|
+
controllers while your component depends on one already?
|
95
|
+
|
96
|
+
This library is an attempt to make this kind of developer experience while
|
97
|
+
making reusable components more convention over configuration.
|
98
|
+
|
23
99
|
## Protos::Component
|
24
100
|
|
25
|
-
A protos component follows
|
26
|
-
components in your app
|
101
|
+
A protos component follows 3 conventions that make them easy to work with as
|
102
|
+
components in your app:
|
103
|
+
|
104
|
+
- [Slots and themes](#slots-and-themes)
|
105
|
+
- [Attrs and default attrs](#attrs-and-default-attrs)
|
106
|
+
- [Params and options](#params-and-options)
|
27
107
|
|
28
108
|
Every UI component library will have a tension between being too general to fit
|
29
109
|
in your app or too narrow to be useful. Making components that look good out of
|
@@ -32,20 +112,18 @@ the box can make them hard to customize.
|
|
32
112
|
We try and resolve this tension by making these components have a minimal style
|
33
113
|
that can be easily overridden using some ergonomic conventions.
|
34
114
|
|
35
|
-
There are 3 core conventions:
|
36
|
-
- [Slots and themes](#slots-and-themes)
|
37
|
-
- [Attrs and default attrs](#attrs-and-default-attrs)
|
38
|
-
- [Params and options](#params-and-options)
|
39
|
-
|
40
115
|
### Slots and themes
|
41
116
|
|
42
|
-
Components are styled with `css` slots that
|
43
|
-
a `theme`.
|
117
|
+
Components are styled with `css` slots that get their values from a simple hash
|
118
|
+
we call a `theme`.
|
119
|
+
|
120
|
+
You define a `theme` for your component by defining a `#theme` method that
|
121
|
+
returns a hash.
|
122
|
+
|
123
|
+
Users of your components can override, merge, or remove parts of your theme by
|
124
|
+
passing in their own as an argument to the component. Another nice benefit is
|
125
|
+
that your markup doesn't get overwhelmed horizontally with your css classes.
|
44
126
|
|
45
|
-
You define a theme for your component by defining a `#theme` method that returns
|
46
|
-
a hash. This hash will be merged with any theme provided when rendering your
|
47
|
-
component. This allows you to easily override styles for your components
|
48
|
-
depending on their context.
|
49
127
|
|
50
128
|
```ruby
|
51
129
|
class List < Protos::Component
|
@@ -58,7 +136,7 @@ class List < Protos::Component
|
|
58
136
|
|
59
137
|
def theme
|
60
138
|
{
|
61
|
-
list:
|
139
|
+
list: "space-y-4", # We can use `#tokens` from phlex (recommended)
|
62
140
|
item: "font-bold text-2xl" # Or just plain old strings
|
63
141
|
}
|
64
142
|
end
|
@@ -66,7 +144,10 @@ end
|
|
66
144
|
```
|
67
145
|
|
68
146
|
Using a theme and css slots allows us to easily override any part of a component
|
69
|
-
when we render
|
147
|
+
when we render.
|
148
|
+
|
149
|
+
Here we are passing in our own theme. The default behavior is to add these
|
150
|
+
styles on to the theme, rather than replacing them.
|
70
151
|
|
71
152
|
```ruby
|
72
153
|
render List.new(
|
@@ -77,7 +158,12 @@ render List.new(
|
|
77
158
|
)
|
78
159
|
```
|
79
160
|
|
80
|
-
|
161
|
+
When the component is rendered the `tailwind_merge` gem will also prune any
|
162
|
+
duplicate unneeded styles.
|
163
|
+
|
164
|
+
For example even though the themes `list` key would be added together to become
|
165
|
+
`space-y-4 space-y-8`, the `tailwind_merge` gem will prune it down to just
|
166
|
+
`space-y-8` as the two styles conflict.
|
81
167
|
|
82
168
|
```html
|
83
169
|
<ul class="space-y-8">
|
@@ -119,8 +205,8 @@ The new `css[:item]` slot would be:
|
|
119
205
|
<li class="font-bold">Item 1</li>
|
120
206
|
```
|
121
207
|
|
122
|
-
If you want to change the method we define our default theme you can
|
123
|
-
`theme_method` on the class:
|
208
|
+
If you want to change the method we define our default theme under you can
|
209
|
+
override the `theme_method` on the class:
|
124
210
|
|
125
211
|
```ruby
|
126
212
|
class List < Protos::Component
|
@@ -142,12 +228,15 @@ end
|
|
142
228
|
### Attrs and default attrs
|
143
229
|
|
144
230
|
By convention, all components spread in an `attrs` hash on their outermost
|
145
|
-
element of the component.
|
231
|
+
element of the component. There is no rule for this, but it makes them feel more
|
232
|
+
naturally like native html elements when you render them.
|
146
233
|
|
147
|
-
By doing this we enable
|
234
|
+
By doing this we enable 3 main conveniences:
|
148
235
|
1. We can pass a `class` keyword when initializing the component which will be
|
149
236
|
merged safely into the `css[:container]` slot
|
150
|
-
2. We can
|
237
|
+
2. We can pass any html attributes we want to the element such as `id`, `data`
|
238
|
+
etc and it will just work
|
239
|
+
3. We can add default attributes that are safely merged with any provided to the
|
151
240
|
component when its being initialized
|
152
241
|
|
153
242
|
```ruby
|
@@ -175,12 +264,15 @@ class List < Protos::Component
|
|
175
264
|
end
|
176
265
|
```
|
177
266
|
|
178
|
-
`#attrs` will by default merge the `class` keyword into the
|
179
|
-
slot which we define in our theme.
|
267
|
+
`#attrs` returns a hash which will by default merge the `class` keyword into the
|
268
|
+
`css[:container]` slot which we define in our theme. The `ul` elements class
|
269
|
+
would be `space-y-4` as that is the `css[:container]` on our theme.
|
270
|
+
|
271
|
+
Special html options (`class`, `data`) will be safely merged.
|
180
272
|
|
181
|
-
|
182
|
-
|
183
|
-
|
273
|
+
For examples, the component above defines a list controller. If we passed our
|
274
|
+
own controller into data when we initialize, the component's data-controller
|
275
|
+
attribute would be appended to:
|
184
276
|
|
185
277
|
```ruby
|
186
278
|
render List.new(
|
@@ -194,6 +286,9 @@ That would output both controllers to the DOM element:
|
|
194
286
|
<ul data-controller="list tooltip">
|
195
287
|
```
|
196
288
|
|
289
|
+
This makes it very convenient to add functionality to basic components without
|
290
|
+
overriding their core behavior or having to modify/override their class.
|
291
|
+
|
197
292
|
If we wanted to, just like for our theme we can change the method from
|
198
293
|
`default_attrs` by defining the `default_attrs_method` on the class:
|
199
294
|
|
@@ -224,25 +319,27 @@ class List < Protos::Component
|
|
224
319
|
end
|
225
320
|
```
|
226
321
|
|
322
|
+
This makes our initialization declarative and easy to extend without having to
|
323
|
+
consider how to call `super` in the initializer.
|
324
|
+
|
227
325
|
The following keywords are reserved in the base class:
|
228
326
|
|
229
327
|
- `class`
|
230
328
|
- `theme`
|
231
329
|
- `html_options`
|
232
330
|
|
331
|
+
You are free to add whatever positional or keyword arguments you like as long as
|
332
|
+
they don't directly conflict with those names.
|
333
|
+
|
233
334
|
## Putting it all together
|
234
335
|
|
235
|
-
|
336
|
+
Lets revisit the example of our `Navbar` component:
|
236
337
|
|
237
338
|
```ruby
|
238
339
|
require "protos"
|
239
340
|
|
240
341
|
class Navbar < Protos::Component
|
241
342
|
def view_template
|
242
|
-
# **attrs will add:
|
243
|
-
# - Any html options defined on the component initialization such as data,
|
244
|
-
# role, for, etc..
|
245
|
-
# - Class will be added to the css[:container] and applied
|
246
343
|
header(**attrs) do
|
247
344
|
h1(class: css[:heading]) { "Hello world" }
|
248
345
|
h2(class: css[:subtitle]) { "With a subtitle" }
|
@@ -259,19 +356,19 @@ class Navbar < Protos::Component
|
|
259
356
|
|
260
357
|
def theme
|
261
358
|
{
|
262
|
-
container:
|
263
|
-
|
264
|
-
|
265
|
-
"items-center",
|
266
|
-
"gap-sm"
|
267
|
-
),
|
268
|
-
heading: tokens("text-2xl", "font-bold"),
|
269
|
-
subtitle: tokens("text-base")
|
359
|
+
container: "flex justify-between items-center gap-sm",
|
360
|
+
heading: "text-2xl font-bold",
|
361
|
+
subtitle: "text-sm"
|
270
362
|
}
|
271
363
|
end
|
272
364
|
end
|
365
|
+
```
|
273
366
|
|
274
|
-
|
367
|
+
Now all the concerns about adding in our behavior, styles, etc are handled for
|
368
|
+
us by convention:
|
369
|
+
|
370
|
+
```ruby
|
371
|
+
render Navbar.new(
|
275
372
|
# This will add to the component's css[:container] slot
|
276
373
|
class: "my-sm",
|
277
374
|
# This will add the controller and not remove
|
@@ -283,8 +380,6 @@ component = Navbar.new(
|
|
283
380
|
subtitle!: "text-xl" # We can override the entire slot
|
284
381
|
}
|
285
382
|
)
|
286
|
-
|
287
|
-
puts component.call
|
288
383
|
```
|
289
384
|
|
290
385
|
Which produces the following html:
|
@@ -308,7 +403,7 @@ If bundler is not being used to manage dependencies, install the gem by executin
|
|
308
403
|
|
309
404
|
## Usage
|
310
405
|
|
311
|
-
Setup [
|
406
|
+
Setup [TailwindCSS](https://tailwindcss.com/), [DaisyUI](https://daisyui.com)
|
312
407
|
and add the protos path to your content.
|
313
408
|
|
314
409
|
```
|
@@ -316,7 +411,7 @@ npm install -D tailwindcss postcss autoprefixer daisyui
|
|
316
411
|
npx tailwindcss init
|
317
412
|
```
|
318
413
|
|
319
|
-
Then we need to add the protos path to the `content` of our
|
414
|
+
Then we need to add the protos path to the `content` of our tailwind config
|
320
415
|
so tailwind will read the styles defined in the Protos gem.
|
321
416
|
|
322
417
|
Protos also uses semantic spacing such as `p-sm` or `m-md` instead of set
|
@@ -344,15 +439,6 @@ module.exports = {
|
|
344
439
|
lg: "var(--spacing-lg)",
|
345
440
|
xl: "var(--spacing-xl)",
|
346
441
|
},
|
347
|
-
// If you use % based spacing you might want different spacing
|
348
|
-
// for any vertical gaps to prevent overflow
|
349
|
-
gap: {
|
350
|
-
xs: "var(--spacing-gap-xs)",
|
351
|
-
sm: "var(--spacing-gap-sm)",
|
352
|
-
md: "var(--spacing-gap-md)",
|
353
|
-
lg: "var(--spacing-gap-lg)",
|
354
|
-
xl: "var(--spacing-gap-xl)",
|
355
|
-
},
|
356
442
|
},
|
357
443
|
}
|
358
444
|
// ....
|
@@ -388,11 +474,12 @@ end
|
|
388
474
|
|
389
475
|
## Building your own components
|
390
476
|
|
391
|
-
You can override components simply by redefining the class in your
|
477
|
+
You can override components simply by redefining sub-classing the class in your
|
478
|
+
own app:
|
392
479
|
|
393
480
|
```ruby
|
394
|
-
module
|
395
|
-
class Swap < Component
|
481
|
+
module Components
|
482
|
+
class Swap < Protos::Component
|
396
483
|
private
|
397
484
|
|
398
485
|
def on(...)
|
@@ -408,8 +495,22 @@ module Protos
|
|
408
495
|
end
|
409
496
|
```
|
410
497
|
|
411
|
-
|
412
|
-
|
498
|
+
But its much better to avoid the sub-classing and just render the component
|
499
|
+
inside of your own:
|
500
|
+
|
501
|
+
```ruby
|
502
|
+
module Components
|
503
|
+
class Swap < ApplicationComponent
|
504
|
+
def view_template
|
505
|
+
render Protos::Swap.new do |c|
|
506
|
+
# ....
|
507
|
+
end
|
508
|
+
end
|
509
|
+
end
|
510
|
+
end
|
511
|
+
```
|
512
|
+
|
513
|
+
You could use `Proto::List` to create your own list and even use
|
413
514
|
`Phlex::DeferredRender` to make the API more convenient.
|
414
515
|
|
415
516
|
Let's create a list component with headers and actions:
|
data/lib/protos/attributes.rb
CHANGED
@@ -4,11 +4,6 @@ module Protos
|
|
4
4
|
class Attributes
|
5
5
|
# DOCS: A class that represents the attributes of a component. This would be
|
6
6
|
# all html options except for `class` and `theme`.
|
7
|
-
#
|
8
|
-
# This class is responsible for safely merging in both user supplied and
|
9
|
-
# default attributes. When a user adds { data: { controller: "foo" }} to
|
10
|
-
# their component. This will merge the value in so that any default
|
11
|
-
# controllers do not get overridden.
|
12
7
|
|
13
8
|
def initialize(attrs = {}, **kwargs)
|
14
9
|
@attrs = attrs.merge!(kwargs)
|
@@ -34,19 +29,7 @@ module Protos
|
|
34
29
|
private
|
35
30
|
|
36
31
|
def mix(hash, *hashes)
|
37
|
-
|
38
|
-
result.merge!(hash) do |_key, a, b| # rubocop:disable Metrics/ParameterLists
|
39
|
-
next a unless b
|
40
|
-
next a if a == b
|
41
|
-
|
42
|
-
case [a, b]
|
43
|
-
in String, String then "#{a} #{b}"
|
44
|
-
in Array, Array then a + b
|
45
|
-
in Hash, Hash then mix(a, b)
|
46
|
-
else b
|
47
|
-
end
|
48
|
-
end
|
49
|
-
end
|
32
|
+
Mix.call(hash, *hashes)
|
50
33
|
end
|
51
34
|
end
|
52
35
|
end
|
data/lib/protos/component.rb
CHANGED
@@ -27,14 +27,14 @@ module Protos
|
|
27
27
|
option :html_options, default: -> { {} }, reader: false
|
28
28
|
|
29
29
|
# Adds non-defined options to the html_options hash
|
30
|
-
def initialize(
|
30
|
+
def initialize(*, **kwargs, &)
|
31
31
|
defined_keys = self.class.dry_initializer.definitions.keys
|
32
32
|
defined, undefined =
|
33
33
|
kwargs
|
34
34
|
.partition { |key, _| defined_keys.include?(key) }
|
35
35
|
.map!(&:to_h)
|
36
36
|
|
37
|
-
super(
|
37
|
+
super(*, html_options: undefined, **defined, &)
|
38
38
|
end
|
39
39
|
|
40
40
|
private
|
data/lib/protos/mix.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Protos
|
4
|
+
class Mix
|
5
|
+
# DOCS: This class is responsible for safely merging in both user supplied
|
6
|
+
# and default attributes. When a user adds { data: { controller: "foo" }} to
|
7
|
+
# their component. This will merge the value in so that any default
|
8
|
+
# controllers do not get overridden.
|
9
|
+
|
10
|
+
MERGEABLE_ATTRIBUTES = Set.new(%i[class data]).freeze
|
11
|
+
|
12
|
+
def self.call(...) = new.call(...)
|
13
|
+
|
14
|
+
def call(old_hash, *hashes)
|
15
|
+
hashes
|
16
|
+
.compact
|
17
|
+
.each_with_object(old_hash) do |new_hash, result|
|
18
|
+
merge(result, new_hash, top_level: true)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def merge(old_hash, new_hash, top_level: false) # rubocop:disable Metrics/PerceivedComplexity
|
25
|
+
old_hash.merge!(new_hash) do |key, old, new|
|
26
|
+
next old unless new
|
27
|
+
next old if old == new
|
28
|
+
next new if top_level && !MERGEABLE_ATTRIBUTES.include?(key)
|
29
|
+
|
30
|
+
case [old, new]
|
31
|
+
in String, String then "#{old} #{new}"
|
32
|
+
in Array, Array then old + new
|
33
|
+
in Hash, Hash then merge(old, new)
|
34
|
+
else new
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/protos/version.rb
CHANGED
data/lib/protos.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: protos
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nolan J Tait
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-09-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: dry-core
|
@@ -149,6 +149,7 @@ files:
|
|
149
149
|
- lib/protos/hero/overlay.rb
|
150
150
|
- lib/protos/list.rb
|
151
151
|
- lib/protos/list/item.rb
|
152
|
+
- lib/protos/mix.rb
|
152
153
|
- lib/protos/modal.rb
|
153
154
|
- lib/protos/modal/close_button.rb
|
154
155
|
- lib/protos/modal/dialog.rb
|