protos 0.5.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 875e55f36d03988e78d0e6834e840094b089ee1e755c003b6483a07a503bfbf4
4
- data.tar.gz: 6b0bdbfc22451928316c73dd931cdeebe3e47cf20c596b3d5318b3145f51b5bc
3
+ metadata.gz: '0796561a80e75fb39f8b35ec75257104adc90b21227ef1525876576d10415269'
4
+ data.tar.gz: 394a8ee190d1d2647476db45d2d9b9edb156bc42a3935898d6f1deca35261326
5
5
  SHA512:
6
- metadata.gz: 62c59568bcf8631d3d9c40de57ddb6b1df2f92183c0e9aa92e8be6b0da7d6e325e440a869eda3f1fd0cb93f651f9db8598a9914cc89b6454a1cda7f5a04517fc
7
- data.tar.gz: 2ef45fba8e402133e4345fa91186713dc864e11d737bf3200548340cf7ed3d115e08b406b6d5c98eab8d568d4178b37d1b5e43fa13569dcc873fda6a8f338be9
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 some conventions that make them easy to work with as
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 are filled with values from
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: tokens("space-y-4"), # We can use `#tokens` from phlex (recommended)
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
- This would combine the default and our theme using tailwind\_merge:
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 override the
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 2 main conveniences:
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 add default attributes that are safely merged with any provided to the
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 `css[:container]`
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
- Special html options will be safely merged. For examples, the component above
182
- defines a list controller. If we passed our own controller into data when we
183
- initialize, the component's data-controller attribute would be appended to:
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
- Here is an example of a small navbar component:
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: tokens(
263
- "flex",
264
- "justify-between",
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
- component = Navbar.new(
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 [Tailwindcss](https://tailwindcss.com/), [daisyUI](https://daisyui.com)
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 tailwindcss config
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 own app:
477
+ You can override components simply by redefining sub-classing the class in your
478
+ own app:
392
479
 
393
480
  ```ruby
394
- module Protos
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
- You could also encapsulate these more primitive proto components into your own
412
- components. You could use `Proto::List` to create your own list and even use
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:
@@ -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
- hashes.each_with_object(hash) do |hash, result|
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
@@ -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(*args, **kwargs, &)
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(*args, html_options: undefined, **defined, &)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Protos
4
- VERSION = "0.5.0"
4
+ VERSION = "0.6.0"
5
5
  end
data/lib/protos.rb CHANGED
@@ -15,6 +15,7 @@ require_relative "protos/types"
15
15
  require_relative "protos/token_list"
16
16
  require_relative "protos/component"
17
17
  require_relative "protos/theme"
18
+ require_relative "protos/mix"
18
19
  require_relative "protos/attributes"
19
20
 
20
21
  # Components
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.5.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-08-27 00:00:00.000000000 Z
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