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 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