vident 0.8.0 → 0.9.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: 6907ba81655f5cf18c500edcdfa43c8786d8b2463573a1dadd0dd51ba1637ea3
4
- data.tar.gz: 72964008e3a0c64172726f4edfe2ba030f66018a08713e8bad529eae4dda45d7
3
+ metadata.gz: 9144ee8a1b3361914b568d5dfbdcc22df506818813573e91ede2302e3a6775aa
4
+ data.tar.gz: 6476408b4f5acf6e4dd40d96539df7fb5588bb225200d3fbd05d35310a7c1c2d
5
5
  SHA512:
6
- metadata.gz: dea9a3d4e70478870b829fe050cfbbcd89dad15866a4a4334d8661d217f56e30a47b81285f279008bfaf762a406e7356ca33d56c607e98a34ddde6b2bf26df13
7
- data.tar.gz: ea963a8ba620e0c510c8e4fadf02eb0293159398c576dbd9554bffc01fd7bb930e7b9d31e2011710ab7a4c41a2929a2e8964cceee2bdfe35827a8dedcad148b4
6
+ metadata.gz: 3098fa53c35329dc0ab5bbae9c4d2ca368ce8d9486fa5cfad29fddf8b006bed162dd8eb89cc1b76d42be91daecb0f0b41f979c866734f5a05f99580936444728
7
+ data.tar.gz: 47dff0e200721e7b48498e13cb27cf58fd7b0da9be6b428be201bb4f73593ffc233a459dc2e0b16f2c925097c65956a274a41c3707c1bfb45be1eccc7b2edbc7
data/README.md CHANGED
@@ -11,7 +11,7 @@ Vident also provides a neat Ruby DSL to make wiring up **Stimulus easier & less
11
11
  # Motivation
12
12
 
13
13
  I love working with Stimulus, but I find manually crafting the data attributes for
14
- targets and actions error prone and tedious. Vident aims to make this process easier
14
+ targets and actions error-prone and tedious. Vident aims to make this process easier
15
15
  and keep me thinking in Ruby.
16
16
 
17
17
  I have been using Vident with `ViewComponent` in production apps for a while now (and recently `Phlex`!)
@@ -19,33 +19,64 @@ and it has been constantly evolving.
19
19
 
20
20
  This gem is a work in progress and I would love to get your feedback and contributions!
21
21
 
22
+ # Vident is a collection of gems
22
23
 
24
+ The core gems are:
23
25
 
24
- **The docs below need updating**
26
+ - [`vident`](https://github.com/stevegeek/vident) to get the base functionality
27
+ - [`vident-typed`](https://github.com/stevegeek/vident-typed) to optionally define typed attributes for your view components
25
28
 
29
+ Gems that provide support for `ViewComponent` and `Phlex`:
30
+
31
+ - [`vident-view_component`](https://github.com/stevegeek/vident-view_component) for using with `ViewComponent` and untyped attributes
32
+ - [`vident-typed-view_component`](https://github.com/stevegeek/vident-typed-view_component) for using with `ViewComponent` and typed attributes
33
+ - [`vident-phlex`](https://github.com/stevegeek/vident-phlex) for using with `Phlex` and untyped attributes
34
+ - [`vident-typed-phlex`](https://github.com/stevegeek/vident-typed-phlex) for using with `Phlex` and typed attributes
35
+
36
+ There is also:
37
+
38
+ - [`vident-typed-minitest`](https://github.com/stevegeek/vident-typed-minitest) to get some test helpers for typed attributes (auto generates inputs to test attributes)
39
+ - [`vident-better_html`](https://github.com/stevegeek/vident-better_html) to support `better_html` if you use it in your Rails app
40
+ - [`vident-tailwind`](https://github.com/stevegeek/vident-tailwind) to get all the benefits of the amazing [`tailwind_merge`](https://github.com/gjtorikian/tailwind_merge/).
26
41
 
27
42
 
43
+ # Things still to do...
44
+
45
+ This is a work in progress. Here's what's left to do for first release:
46
+
47
+ - Iterate on the interfaces and functionality
48
+ - Add tests
49
+ - Make the gem more configurable to fit more use cases
50
+ - Create an example library of a few components for some design system
51
+ - Create a demo app with `lookbook` and those components
52
+ - Add more documentation
53
+
54
+ # About Vident
55
+
28
56
  ## What does Vident provide?
29
57
 
30
- - `Vident::Component`: A mixin for your `ViewComponent` components or `Phlex` components that provides the a helper to create the
31
- root element component (in templated or template-less components).
58
+ - Base classes for your `ViewComponent` components or `Phlex` components that provides a helper to create the
59
+ all important 'root' element component (can be used with templated or template-less components).
60
+
61
+ - implementations of these root components for creating the 'root' element in your view components. Similar to `Primer::BaseComponent` but
62
+ exposes a simple API for configuring and adding Stimulus controllers, targets and actions. The root component also handles deduplication
63
+ of classes, creating a unique ID, setting the element tag type, handling possible overrides set at the render site, and determining stimulus controller identifiers etc
32
64
 
33
- - `Vident::TypedComponent`: like `Vident::Component` but uses `dry-types` to define typed attributes for your components.
65
+ - a way to define attributes for your components, either typed or untyped, with default values and optional validation.
34
66
 
35
67
  ### Various utilities
36
68
 
37
- - `Vident::Tailwind`: a mixin for your vident component which uses [tailwind_merge](https://github.com/gjtorikian/tailwind_merge) to merge TailwindCSS classes
38
- so you can easily override classes when rendering a component.
69
+ Such as...
39
70
 
40
- - `Vident::Caching::CacheKey`: a mixin for your vident component which provides a `cache_key` method that can be used to generate a cache key for
71
+ - for Taiwind users, a mixin for your vident component which uses [tailwind_merge](https://github.com/gjtorikian/tailwind_merge) to merge TailwindCSS classes
72
+ so you can easily override classes when rendering a component.
73
+ - a mixin for your Vident Components which provides a `#cache_key` method that can be used to generate a cache key for
41
74
  fragment caching or etag generation.
75
+ - a test helper for your typed Vident ViewComponents which can be used to generate good and bad attribute/params/inputs
42
76
 
43
- - `Vident::RootComponent::*` which are components for creating the 'root' element in your view components. Similar to `Primer::BaseComponent` but
44
- exposes a simple API for configuring and adding Stimulus controllers, targets and actions. Normally you create these
45
- using the `root` helper method on `Vident::Component`/`Vident::TypedComponent`.
46
-
47
- # Features
77
+ ## All the Features...
48
78
 
79
+ - use Vident with `ViewComponent` or `Phlex` or your own view component system
49
80
  - A helper to create the root HTML element for your component, which then handles creation of attributes.
50
81
  - Component arguments are defined using the `attribute` method which allows you to define default values, (optionally) types and
51
82
  if blank or nil values should be allowed.
@@ -57,474 +88,107 @@ This gem is a work in progress and I would love to get your feedback and contrib
57
88
  having to update the data attributes in your views.
58
89
  - Components are rendered with useful class names and IDs to make debugging easier (autogenerated IDs are 'random' but deterministic so they
59
90
  are the same each time a given view is rendered to avoid content changing/Etag changing).
60
- - (experimental) Support for fragment caching of components (only with ViewComponent and with caveats)
91
+ - (experimental) Support for fragment caching of components (`Vident::Caching` and `Vident::<ViewComponent | Phlex>::Caching`... implementation has caveats)
61
92
  - (experimental) A test helper to make testing components easier by utilising type information from the component arguments to render
62
93
  automatically configured good and bad examples of the component.
63
94
  - (experimental) support for `better_html`
64
95
 
65
96
 
66
- ## Things still to do...
67
-
68
- This is a work in progress. Here's what's left to do for first release:
69
-
70
- - Iterate on the interfaces and functionality
71
- - Add tests
72
- - Make the gem more configurable to fit more use cases
73
- - Create an example library of a few components for some design system
74
- - Create a demo app with `lookbook` and those components
75
- - Add more documentation
76
- - split `vident` into `vident` + `vident-rails` gems (and maybe `vident-rspec`) (Phlex can be used outside of Rails)
77
- - possibly also split into `vident-phlex` and `vident-view_component` gems ?
78
-
79
-
80
-
81
- # Examples
82
-
83
- Before we dive into a specific example note that there are some components implemented with
84
- both ViewComponent and Phlex (with and without Vident) in the `test/dummy`.
85
- - https://github.com/stevegeek/vident/tree/main/test/dummy/app/components
86
- - https://github.com/stevegeek/vident/tree/main/test/dummy/app/views
87
-
88
- Start Rails:
89
-
90
- ```bash
91
- cd test/dummy
92
- bundle install
93
- rails assets:precompile
94
- rails s
95
- ```
96
-
97
- and visit http://localhost:3000
98
-
99
-
100
- ## A Vident component example (without Stimulus)
101
-
102
- First is an example component that uses `Vident::TypedComponent` but no Stimulus features.
103
-
104
- It is an avatar component that can either be displayed as an image or as initials.
105
-
106
- It supports numerous sizes and shapes and can optionally have a border. It also generates a cache key for use in fragment caching or etag generation.
107
-
108
- ```ruby
109
- class AvatarComponent < ViewComponent::Base
110
- include ::Vident::TypedComponent
111
- include ::Vident::Tailwind
112
- include ::Vident::Caching::CacheKey
113
-
114
- no_stimulus_controller
115
- with_cache_key :attributes
116
-
117
- attribute :url, String, allow_nil: true, allow_blank: false
118
- attribute :initials, String, allow_blank: false
119
-
120
- attribute :shape, Symbol, in: %i[circle square], default: :circle
121
-
122
- attribute :border, :boolean, default: false
123
-
124
- attribute :size, Symbol, in: %i[tiny small normal medium large x_large xx_large], default: :normal
125
-
126
- private
127
-
128
- def html_options
129
- if image_avatar?
130
- { class: "inline-block object-contain", src: url, alt: t(".image") }
131
- else
132
- { class: "inline-flex items-center justify-center bg-gray-500" }
133
- end
134
- end
135
-
136
- def element_classes
137
- [size_classes, shape_class, border? ? "border" : ""]
138
- end
139
-
140
- alias_method :image_avatar?, :url?
141
-
142
- def shape_class
143
- (shape == :circle) ? "rounded-full" : "rounded-md"
144
- end
145
-
146
- def size_classes
147
- case size
148
- when :tiny
149
- "w-6 h-6"
150
- when :small
151
- "w-8 h-8"
152
- when :medium
153
- "w-12 h-12"
154
- when :large
155
- "w-14 h-14"
156
- when :x_large
157
- "sm:w-24 sm:h-24 w-16 h-16"
158
- when :xx_large
159
- "sm:w-32 sm:h-32 w-24 h-24"
160
- else
161
- "w-10 h-10"
162
- end
163
- end
164
-
165
- def text_size_class
166
- case size
167
- when :tiny
168
- "text-xs"
169
- when :small
170
- "text-xs"
171
- when :medium
172
- "text-lg"
173
- when :large
174
- "sm:text-xl text-lg"
175
- when :extra_large
176
- "sm:text-2xl text-xl"
177
- else
178
- "text-medium"
179
- end
180
- end
181
- end
182
- ```
183
-
184
- ```erb
185
- <%= render root(
186
- element_tag: image_avatar? ? :img : :div,
187
- html_options: html_options
188
- ) do %>
189
- <% unless image_avatar? %>
190
- <span class="<%= text_size_class %> font-medium leading-none text-white"><%= initials %></span>
191
- <% end %>
192
- <% end %>
193
-
194
- ```
195
-
196
- Example usages:
197
-
198
- ```erb
199
- <!-- These will render -->
200
- <%= render AvatarComponent.new(url: "https://someurl.com/avatar.jpg", initials: "AB" size: :large) %>
201
- <%= render AvatarComponent.new(url: "https://someurl.com/avatar.jpg", html_options: {alt: "My alt text", class: "object-scale-down"}) %>
202
- <%= render AvatarComponent.new(initials: "SG", size: :small) %>
203
- <%= render AvatarComponent.new(initials: "SG", size: :large, html_options: {class: "border-2 border-red-600"}) %>
204
-
205
- <!-- These will raise an error -->
206
- <!-- missing initals -->
207
- <%= render AvatarComponent.new(url: "https://someurl.com/avatar.jpg", size: :large) %>
208
- <!-- initials blank -->
209
- <%= render AvatarComponent.new(initials: "", size: :large) %>
210
- <!-- invalid size -->
211
- <%= render AvatarComponent.new(initials: "SG", size: :foo_bar) %>
212
- ```
213
-
97
+ ## Installation
214
98
 
215
- The following is rendered when used `render AvatarComponent.new(initials: "SG", size: :small, border: true)`:
99
+ This gem (`vident`) provides only base functionality but there are a number of gems that provide additional functionality
100
+ or an "out of the box" experience.
216
101
 
217
- ```html
218
- <div class="avatar-component w-8 h-8 rounded-full border inline-flex items-center justify-center bg-gray-500" id="avatar-component-9790427-12">
219
- <span class="text-xs font-medium leading-none text-white">SG</span>
220
- </div>
221
- ```
102
+ It's a "pick your own adventure" approach. You decide what frameworks and features you want to use
103
+ and add the gems as needed.
222
104
 
223
- The following is rendered when used `render AvatarComponent.new(url: "https://i.pravatar.cc/300", initials: "AB", html_options: {alt: "My alt text", class: "block"})`:
105
+ First, add this line to your application's Gemfile:
224
106
 
225
- ```html
226
- <img src="https://i.pravatar.cc/300" alt="My alt text" class="avatar-component w-10 h-10 rounded-full object-contain block" id="avatar-component-7083941-11">
107
+ ```ruby
108
+ gem 'vident'
227
109
  ```
228
110
 
229
- ----
111
+ Then go on to choose the gems you want to use:
230
112
 
231
- ![Example](examples/avatar.png)
113
+ #### Q1. Do you want to use [`ViewComponent`](https://viewcomponent.org/) or [`Phlex`](https://www.phlex.fun/) for your view components?
232
114
 
233
- ## Another ViewComponent + Vident example with Stimulus
115
+ For ViewComponent use:
234
116
 
235
- Consider the following ERB that might be part of an application's views. The app uses `ViewComponent`, `Stimulus` and `Vident`.
117
+ - [`vident-view_component`](https://github.com/stevegeek/vident-view_component)
236
118
 
237
- The Greeter is a component that displays a text input and a button. When the button is clicked, the text input's value is
238
- used to greet the user. At the same time the button changes to be a 'reset' button, which resets the greeting when clicked again.
119
+ For Phlex use:
239
120
 
240
- ![ex1.gif](examples%2Fex1.gif)
121
+ - [`vident-phlex`](https://github.com/stevegeek/vident-phlex)
241
122
 
242
- ```erb
243
- <%# app/views/home/index.html.erb %>
244
123
 
245
- <!-- ... -->
124
+ Note: you can also use both in the same app.
246
125
 
247
- <!-- render the Greeter ViewComponent (that uses Vident) -->
248
- <%= render ::GreeterComponent.new(cta: "Hey!", html_options: {class: "my-4"}) do |greeter| %>
249
- <%# this component has a slot called `trigger` that renders a `ButtonComponent` (which also uses Vident) %>
250
- <% greeter.trigger(
251
-
252
- # The button component has attributes that are typed
253
- before_clicked: "Greet",
254
- after_clicked: "Greeted! Reset?",
255
-
256
- # A stimulus action is added to the button that triggers the `greet` action on the greeter stimulus controller.
257
- # This action will be added to any defined on the button component itself
258
- actions: [
259
- greeter.action(:click, :greet),
260
- ],
261
-
262
- # We can also override the default button classes of our component, or set other HTML attributes
263
- html_options: {
264
- class: "bg-red-500 hover:bg-red-700"
265
- }
266
- ) %>
267
- <% end %>
268
-
269
- <!-- ... -->
270
- ```
126
+ For example, if you want to use ViewComponent and Phlex in the same app, you might end up with:
271
127
 
272
- The output HTML of the above, using Vident, is:
273
-
274
- ```html
275
- <div class="greeter-component py-2 my-4"
276
- data-controller="greeter-component"
277
- data-greeter-component-pre-click-class="text-md text-gray-500"
278
- data-greeter-component-post-click-class="text-xl text-blue-700"
279
- id="greeter-component-1599855-6">
280
- <input type="text"
281
- data-greeter-component-target="name"
282
- class="shadow appearance-none border rounded py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
283
- <button class="button-component ml-4 whitespace-no-wrap bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded bg-red-500 hover:bg-red-700"
284
- data-controller="button-component"
285
- data-action="click->greeter-component#greet button-component#changeMessage"
286
- data-button-component-after-clicked-message="Greeted! Reset?"
287
- data-button-component-before-clicked-message="Greet"
288
- id="button-component-7799479-7">Hey!</button>
289
- <!-- you can also use the `target_tag` helper to render targets -->
290
- <span class="ml-4 text-md text-gray-500"
291
- data-greeter-component-target="output">
292
- ...
293
- </span>
294
- </div>
128
+ ```ruby
129
+ gem 'vident'
130
+ gem 'vident-view_component'
131
+ gem 'vident-phlex'
295
132
  ```
296
133
 
297
- Let's look at the components in more detail.
134
+ #### Q2. Do you want to build components where the attributes have runtime type checking (powered by [`dry-types`](https://github.com/dry-rb/dry-types))?
298
135
 
299
- The main component is the `GreeterComponent`:
136
+ If yes, then add `vident-typed` to your Gemfile:
300
137
 
301
138
  ```ruby
302
- # app/components/greeter_component.rb
303
-
304
- class GreeterComponent < ViewComponent::Base
305
- include Vident::Component
306
-
307
- renders_one :trigger, ButtonComponent
308
- end
139
+ gem 'vident-typed'
309
140
  ```
310
141
 
311
- ```erb
312
- <%# app/components/greeter_component.html.erb %>
313
-
314
- <%# Rendering the `root` element creates a tag which has stimulus `data-*`s, a unique id & other attributes set. %>
315
- <%# The stimulus controller name (identifier) is derived from the component name, and then used to generate the relavent data attribute names. %>
316
-
317
- <%= render root named_classes: {
318
- pre_click: "text-md text-gray-500", # named classes are exposed to Stimulus as `data-<controller>-<name>-class` attributes
319
- post_click: "text-xl text-blue-700",
320
- html_options: {class: "py-2"}
321
- } do |greeter| %>
322
-
323
- <%# `greeter` is the root element and exposes methods to generate stimulus targets and actions %>
324
- <input type="text"
325
- <%= greeter.as_target(:name) %>
326
- class="shadow appearance-none border rounded py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
327
-
328
- <%# Render the slot %>
329
- <%= trigger %>
330
-
331
- <%# you can also use the `target_tag` helper to render targets %>
332
- <%= greeter.target_tag(
333
- :span,
334
- :output,
335
- # Stimulus named classes can be referenced to set class attributes at render time
336
- class: "ml-4 #{greeter.named_classes(:pre_click)}"
337
- ) do %>
338
- ...
339
- <% end %>
340
- <% end %>
142
+ and then use the relavent `*-typed-*` gems for your chosen view component system:
341
143
 
342
- ```
144
+ - use [`vident-typed-view_component`](https://github.com/stevegeek/vident-typed-view_component)
145
+ - and/or [`vident-typed-phlex`](https://github.com/stevegeek/vident-typed-phlex)
343
146
 
344
- ```js
345
- // app/components/greeter_component_controller.js
346
-
347
- import { Controller } from "@hotwired/stimulus"
348
-
349
- // This is a Stimulus controller that is automatically registered for the `GreeterComponent`
350
- // and is 'sidecar' to the component. You can see that while in the ERB we use Ruby naming conventions
351
- // with snake_case Symbols, here they are converted to camelCase names. We can also just use camelCase
352
- // in the ERB if we want.
353
- export default class extends Controller {
354
- static targets = [ "name", "output" ]
355
- static classes = [ "preClick", "postClick" ]
356
-
357
- greet() {
358
- this.clicked = !this.clicked;
359
- this.outputTarget.classList.toggle(this.preClickClasses, !this.clicked);
360
- this.outputTarget.classList.toggle(this.postClickClasses, this.clicked);
361
-
362
- if (this.clicked)
363
- this.outputTarget.textContent = `Hello, ${this.nameTarget.value}!`
364
- else
365
- this.clear();
366
- }
367
-
368
- clear() {
369
- this.outputTarget.textContent = '...';
370
- this.nameTarget.value = '';
371
- }
372
- }
373
- ```
147
+ Note you must also include the gem for the view component system you are using.
374
148
 
375
- The slot renders a `ButtonComponent` component:
149
+ For example, for ViewComponent, you might end up with:
376
150
 
377
151
  ```ruby
378
- # app/components/button_component.rb
379
-
380
- class ButtonComponent < ViewComponent::Base
381
- # This component uses Vident::TypedComponent which uses dry-types to define typed attributes.
382
- include Vident::TypedComponent
383
-
384
- # The attributes can specify an expected type, a default value and if nil is allowed.
385
- attribute :after_clicked, String, default: "Greeted!"
386
- attribute :before_clicked, String, allow_nil: false
387
-
388
- # This example is a templateless ViewComponent.
389
- def call
390
- # The button is rendered as a <button> tag with an click action on its own controller.
391
- render root(
392
- element_tag: :button,
393
-
394
- # We can define actions as arrays of Symbols, or pass manually manually crafted strings.
395
- # Here we specify the action name only, implying an action on the current components controller
396
- # and the default event type of `click`.
397
- actions: [:change_message],
398
- # Alternatively: [:click, :change_message] or ["click", "changeMessage"] or even "click->button-component#changeMessage"
399
-
400
- # A couple of data values are also set which will be available to the controller
401
- data_maps: [{after_clicked_message: after_clicked, before_clicked_message: before_clicked}],
402
-
403
- # The <button> tag has a default styling set directly on it. Note that
404
- # if not using utility classes, you can style the component using its
405
- # canonical class name (which is equal to the component's stimulus identifier),
406
- # in this case `button-component`.
407
- html_options: {class: "ml-4 whitespace-no-wrap bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"}
408
- ) do
409
- @before_clicked
410
- end
411
- end
412
- end
152
+ gem 'vident'
153
+ gem 'vident-view_component'
154
+ gem 'vident-typed'
155
+ gem 'vident-typed-view_component'
413
156
  ```
414
157
 
415
- ```js
416
- // app/components/button_component_controller.js
417
-
418
- import { Controller } from "@hotwired/stimulus"
419
-
420
- export default class extends Controller {
421
- // The action is in camelCase.
422
- changeMessage() {
423
- this.clicked = !this.clicked;
424
- // The data attributes have their naming convention converted to camelCase.
425
- this.element.textContent = this.clicked ? this.data.get("afterClickedMessage") : this.data.get("beforeClickedMessage");
426
- }
427
- }
428
-
429
- ```
430
-
431
- ## Installation
432
-
433
- This gem (`vident`) provides only base functionality but there are a number of gems that provide additional functionality
434
- or a "out of the box" experience.
435
-
436
- It's a "pick your own adventure" approach. You decide what frameworks and features you want to use
437
- and add the gems as needed.
438
-
439
- Start by asking yorself these questions:
440
-
441
- _Q1. Do you want to use [`ViewComponent`](https://viewcomponent.org/) or [`Phlex`](https://www.phlex.fun/) for your view components?_
442
-
443
- _Q2. Do you want to use attributes with runtime type checking (powered by [`dry-types`](https://github.com/dry-rb/dry-types)), or not?_
444
-
445
- Depending on your answer to Q1, you can choose from either the `*-view_component` or `*-phlex` gems,
446
- then depending on your answer to Q2, you can choose from either the untyped or `*-typed-*` gems.
447
-
448
- Note you can also use some or all of them in the same app.
449
-
450
- - [`vident-view_component`](https://github.com/stevegeek/vident-view_component) for using with `ViewComponent` and untyped attributes
451
- - [`vident-typed-view_component`](https://github.com/stevegeek/vident-typed-view_component) for using with `ViewComponent` and typed attributes
452
- - [`vident-phlex`](https://github.com/stevegeek/vident-phlex) for using with `Phlex` and untyped attributes
453
- - [`vident-typed-phlex`](https://github.com/stevegeek/vident-typed-phlex) for using with `Phlex` and typed attributes
454
-
455
- _Q3. Do you use or want to use [BetterHTML](https://github.com/Shopify/better-html) in your Rails project?_
158
+ #### Q3. Do you use or want to use [BetterHTML](https://github.com/Shopify/better-html) in your Rails project?
456
159
 
457
160
  If yes, then include [`vident-better_html`](https://github.com/stevegeek/vident-better_html) in your Gemfile alongside `better_html` and your vident gems of choice.
458
- Note that `vident-better_html` automatically enables `better_html` support in Vident root components.
459
-
460
- _Q4. Do you use or want to use [TailwindCSS](https://tailwindcss.com/)?_
461
-
462
- If yes, then consider adding [`vident-tailwind`](https://github.com/stevegeek/vident-tailwind) to your Gemfile alongside your vident gems of choice. When creating
463
- your components you can then include `Vident::Tailwind` to get all the benefits of the amazing [`tailwind_merge`](https://github.com/gjtorikian/tailwind_merge/).
464
-
465
- Finally if none of the above gems suit your needs, you can always just use base `vident` gems to roll your own
466
- solution:
467
-
468
- - [`vident`](https://github.com/stevegeek/vident) to get the base functionality to mix with your own view component system
469
- - [`vident-typed`](https://github.com/stevegeek/vident-typed) to define typed attributes for your own view component system
470
-
471
-
472
- And then execute:
473
-
474
- $ bundle install
475
-
476
- ## Making 'sidecar' Stimulus Controllers work
477
-
478
- ### When using `stimulus-rails`, `sprockets-rails` & `importmap-rails`
479
-
480
- Pin any JS modules from under `app/views` and `app/components` which are sidecar with their respective components.
481
-
482
- Add to `config/importmap.rb`:
483
161
 
484
162
  ```ruby
485
- components_directories = [Rails.root.join("app/components"), Rails.root.join("app/views")]
486
- components_directories.each do |components_path|
487
- prefix = components_path.basename.to_s
488
- components_path.glob("**/*_controller.js").each do |controller|
489
- name = controller.relative_path_from(components_path).to_s.remove(/\.js$/)
490
- pin "#{prefix}/#{name}", to: name
491
- end
492
- end
163
+ ...
164
+ gem 'better_html'
165
+ gem 'vident-better_html'
493
166
  ```
494
167
 
495
- Note we don't use `pin_all_from` as it is meant to work with a subdirectory in `assets.paths`
496
- See this for more: https://stackoverflow.com/a/73228193/268602
168
+ Note that `vident-better_html` automatically enables `better_html` support in Vident root components.
497
169
 
498
- Then we need to ensure that sprockets picks up those files in build, so add
499
- to the `app/assets/config/manifest.js`:
170
+ ### Q4. Do you use or want to use [TailwindCSS](https://tailwindcss.com/)?
500
171
 
501
- ```js
502
- //= link_tree ../../components .js
503
- //= link_tree ../../views .js
504
- ```
505
-
506
- We also need to add to `assets.paths`. Add to your to `config/application.rb`
172
+ If yes, then consider adding [`vident-tailwind`](https://github.com/stevegeek/vident-tailwind) to your Gemfile alongside your vident gems of choice.
507
173
 
508
174
  ```ruby
509
- config.importmap.cache_sweepers.append(Rails.root.join("app/components"), Rails.root.join("app/views"))
510
- config.assets.paths.append("app/components", "app/views")
175
+ ...
176
+ gem 'vident-tailwind'
511
177
  ```
512
178
 
513
- ### When using `webpacker`
514
-
515
- TODO
179
+ When creating your components you can then include `Vident::Tailwind` to get all the benefits of the amazing [`tailwind_merge`](https://github.com/gjtorikian/tailwind_merge/).
516
180
 
517
- ### When using `propshaft`
181
+ ### Q5. Did none of the above gems suit your needs?
518
182
 
519
- TODO
183
+ You can always just use base `vident` gems and then roll your own solutions:
520
184
 
521
- ## Using TypeScript for Stimulus Controllers
185
+ - [`vident`](https://github.com/stevegeek/vident) to get the base functionality to mix with your own view component system
186
+ - [`vident-typed`](https://github.com/stevegeek/vident-typed) to define typed attributes for your own view component system
522
187
 
523
- TODO
524
188
 
525
- ## Usage
189
+ ## Documentation
526
190
 
527
- TODO: Write usage instructions here
191
+ See the [docs](docs/) directory and visit the individual gem pages for more information.
528
192
 
529
193
  ## Development
530
194
 
@@ -532,8 +196,6 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
532
196
 
533
197
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
534
198
 
535
-
536
-
537
199
  ## Contributing
538
200
 
539
201
  Bug reports and pull requests are welcome on GitHub at https://github.com/stevegeek/vident. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/vident/blob/master/CODE_OF_CONDUCT.md).
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vident
4
+ # Rails fragment caching works by either expecting the cached key object to respond to `cache_key` or for that object
5
+ # to be an array or hash.
6
+ module Caching
7
+ extend ActiveSupport::Concern
8
+
9
+ class_methods do
10
+ def inherited(subclass)
11
+ subclass.instance_variable_set(:@named_cache_key_attributes, @named_cache_key_attributes.clone)
12
+ super
13
+ end
14
+
15
+ def with_cache_key(*attrs, name: :_collection)
16
+ # Add view file to cache key
17
+ attrs << :component_modified_time
18
+ attrs << :attributes
19
+ named_cache_key_includes(name, *attrs.uniq)
20
+ end
21
+
22
+ attr_reader :named_cache_key_attributes
23
+
24
+ # Components can be used with fragment caching, but you need to be careful! Read on...
25
+ #
26
+ # <% cache component do %>
27
+ # <%= render component %>
28
+ # <% end %>
29
+ #
30
+ # The most important point is that Rails cannot track dependencies on the component itself, so you need to
31
+ # be careful to be explicit on the attributes, and manually specify any sub Viewcomponent dependencies that the
32
+ # component has. The assumption is that the subcomponent takes any attributes from the parent, so the cache key
33
+ # depends on the parent component attributes. Otherwise changes to the parent or sub component views/Ruby class
34
+ # will result in different cache keys too. Of course if you invalidate all cache keys with a modifier on deploy
35
+ # then no need to worry about changing the cache key on component changes, only on attribute/data changes.
36
+ #
37
+ # A big caveat is that the cache key cannot depend on anything related to the view_context of the component (such
38
+ # as `helpers` as the key is created before the rending pipline is invoked (which is when the view_context is set).
39
+ def depends_on(*klasses)
40
+ @component_dependencies ||= []
41
+ @component_dependencies += klasses
42
+ end
43
+
44
+ attr_reader :component_dependencies
45
+
46
+ def component_modified_time
47
+ return @component_modified_time if Rails.env.production? && @component_modified_time
48
+
49
+ raise StandardError, "Must implement current_component_modified_time" unless respond_to?(:current_component_modified_time)
50
+
51
+ # FIXME: This could stack overflow if there are circular dependencies
52
+ deps = component_dependencies&.map(&:component_modified_time)&.join("-") || ""
53
+ @component_modified_time = deps + current_component_modified_time
54
+ end
55
+
56
+ private
57
+
58
+ def named_cache_key_includes(name, *attrs)
59
+ define_cache_key_method unless @named_cache_key_attributes
60
+ @named_cache_key_attributes ||= {}
61
+ @named_cache_key_attributes[name] = attrs
62
+ end
63
+
64
+ def define_cache_key_method
65
+ # If the presenter defines cache key setup then define the method. Otherwise Rails assumes this
66
+ # will return a valid key if the class will respond to this
67
+ define_method :cache_key do |n = :_collection|
68
+ if defined?(@cache_key)
69
+ return @cache_key[n] if @cache_key.key?(n)
70
+ else
71
+ @cache_key ||= {}
72
+ end
73
+ generate_cache_key(n)
74
+ @cache_key[n]
75
+ end
76
+ end
77
+ end
78
+
79
+ # Component modified time which is combined with other cache key attributes to generate cache key for an instance
80
+ def component_modified_time
81
+ self.class.component_modified_time
82
+ end
83
+
84
+ def cacheable?
85
+ respond_to? :cache_key
86
+ end
87
+
88
+ def cache_key_modifier
89
+ ENV["RAILS_CACHE_ID"]
90
+ end
91
+
92
+ def cache_keys_for_sources(key_attributes)
93
+ sources = key_attributes.flat_map { |n| n.is_a?(Proc) ? instance_eval(&n) : send(n) }
94
+ sources.compact.map do |item|
95
+ next if item == self
96
+ generate_item_cache_key_from(item)
97
+ end
98
+ end
99
+
100
+ def generate_item_cache_key_from(item)
101
+ if item.respond_to? :cache_key_with_version
102
+ item.cache_key_with_version
103
+ elsif item.respond_to? :cache_key
104
+ item.cache_key
105
+ elsif item.is_a?(String)
106
+ Digest::SHA1.hexdigest(item)
107
+ else
108
+ Digest::SHA1.hexdigest(Marshal.dump(item))
109
+ end
110
+ end
111
+
112
+ def generate_cache_key(index)
113
+ key_attributes = self.class.named_cache_key_attributes[index]
114
+ return nil unless key_attributes
115
+ key = "#{self.class.name}/#{cache_keys_for_sources(key_attributes).join("/")}"
116
+ raise StandardError, "Cache key for key #{key} is blank!" if key.blank?
117
+ @cache_key[index] = cache_key_modifier.present? ? "#{key}/#{cache_key_modifier}" : key
118
+ end
119
+ end
120
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vident
4
- VERSION = "0.8.0"
4
+ VERSION = "0.9.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vident
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephen Ierodiaconou
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-03-31 00:00:00.000000000 Z
11
+ date: 2023-08-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -46,6 +46,7 @@ files:
46
46
  - lib/vident.rb
47
47
  - lib/vident/attributes/not_typed.rb
48
48
  - lib/vident/base.rb
49
+ - lib/vident/caching.rb
49
50
  - lib/vident/component.rb
50
51
  - lib/vident/engine.rb
51
52
  - lib/vident/root_component.rb
@@ -73,7 +74,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
73
74
  - !ruby/object:Gem::Version
74
75
  version: '0'
75
76
  requirements: []
76
- rubygems_version: 3.4.6
77
+ rubygems_version: 3.4.10
77
78
  signing_key:
78
79
  specification_version: 4
79
80
  summary: Vident is the base of your design system implementation, which provides helpers