vident-typed-view_component 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8b452999e699b9280843995877688131d0f5340ce7677a2daa255cc205528cde
4
+ data.tar.gz: d443df28c20a174dfbd8e49dc1e2bad9494ba5036628bfa9e649baf9f0fe1a24
5
+ SHA512:
6
+ metadata.gz: 02f8b2eb878cbdef3c01a55ef39e2b0c67d633d9f5bb0ac8fd80967b1561a6ee75ecb547f79ce9ec11c4d2795da93fe3c4fff08de3a5583edd08b5fe98a6c03c
7
+ data.tar.gz: 5806003e62d04293b81a76f3b790bda373dc8dc0bd4fd9669cfb409d2bf6835587703600fe416173ca17c782d1b75c96eabcbdef81add5803cc35aa80718734c
data/README.md ADDED
@@ -0,0 +1,372 @@
1
+ # Vident::Typed::ViewComponent
2
+ Short description and motivation.
3
+
4
+
5
+ # Examples
6
+
7
+ Before we dive into a specific example note that there are some components implemented in `test/dummy/app/components`.
8
+
9
+ Try them out by starting Rails:
10
+
11
+ ```bash
12
+ cd test/dummy
13
+ bundle install
14
+ rails assets:precompile
15
+ rails s
16
+ ```
17
+
18
+ and visiting http://localhost:3000
19
+
20
+
21
+ ## A Vident component example (without Stimulus)
22
+
23
+ First is an example component that uses `Vident::Typed::ViewComponent::Base` but no Stimulus features.
24
+
25
+ It is an avatar component that can either be displayed as an image or as initials.
26
+
27
+ 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.
28
+
29
+ ```ruby
30
+ class AvatarComponent < ::Vident::Typed::ViewComponent::Base
31
+ include ::Vident::Tailwind
32
+ include ::Vident::ViewComponent::Caching
33
+
34
+ no_stimulus_controller
35
+ with_cache_key :attributes
36
+
37
+ attribute :url, String, allow_nil: true, allow_blank: false
38
+ attribute :initials, String, allow_blank: false
39
+
40
+ attribute :shape, Symbol, in: %i[circle square], default: :circle
41
+
42
+ attribute :border, :boolean, default: false
43
+
44
+ attribute :size, Symbol, in: %i[tiny small normal medium large x_large xx_large], default: :normal
45
+
46
+ private
47
+
48
+ def default_html_options
49
+ if image_avatar?
50
+ { class: "inline-block object-contain", src: url, alt: t(".image") }
51
+ else
52
+ { class: "inline-flex items-center justify-center bg-gray-500" }
53
+ end
54
+ end
55
+
56
+ def element_classes
57
+ [size_classes, shape_class, border? ? "border" : ""]
58
+ end
59
+
60
+ alias_method :image_avatar?, :url?
61
+
62
+ def shape_class
63
+ (shape == :circle) ? "rounded-full" : "rounded-md"
64
+ end
65
+
66
+ def size_classes
67
+ case size
68
+ when :tiny
69
+ "w-6 h-6"
70
+ when :small
71
+ "w-8 h-8"
72
+ when :medium
73
+ "w-12 h-12"
74
+ when :large
75
+ "w-14 h-14"
76
+ when :x_large
77
+ "sm:w-24 sm:h-24 w-16 h-16"
78
+ when :xx_large
79
+ "sm:w-32 sm:h-32 w-24 h-24"
80
+ else
81
+ "w-10 h-10"
82
+ end
83
+ end
84
+
85
+ def text_size_class
86
+ case size
87
+ when :tiny
88
+ "text-xs"
89
+ when :small
90
+ "text-xs"
91
+ when :medium
92
+ "text-lg"
93
+ when :large
94
+ "sm:text-xl text-lg"
95
+ when :extra_large
96
+ "sm:text-2xl text-xl"
97
+ else
98
+ "text-medium"
99
+ end
100
+ end
101
+ end
102
+ ```
103
+
104
+ ```erb
105
+ <%= render root(
106
+ element_tag: image_avatar? ? :img : :div,
107
+ html_options: default_html_options
108
+ ) do %>
109
+ <% unless image_avatar? %>
110
+ <span class="<%= text_size_class %> font-medium leading-none text-white"><%= initials %></span>
111
+ <% end %>
112
+ <% end %>
113
+
114
+ ```
115
+
116
+ Example usages:
117
+
118
+ ```erb
119
+ <!-- These will render -->
120
+ <%= render AvatarComponent.new(url: "https://someurl.com/avatar.jpg", initials: "AB" size: :large) %>
121
+ <%= render AvatarComponent.new(url: "https://someurl.com/avatar.jpg", html_options: {alt: "My alt text", class: "object-scale-down"}) %>
122
+ <%= render AvatarComponent.new(initials: "SG", size: :small) %>
123
+ <%= render AvatarComponent.new(initials: "SG", size: :large, html_options: {class: "border-2 border-red-600"}) %>
124
+
125
+ <!-- These will raise an error -->
126
+ <!-- missing initals -->
127
+ <%= render AvatarComponent.new(url: "https://someurl.com/avatar.jpg", size: :large) %>
128
+ <!-- initials blank -->
129
+ <%= render AvatarComponent.new(initials: "", size: :large) %>
130
+ <!-- invalid size -->
131
+ <%= render AvatarComponent.new(initials: "SG", size: :foo_bar) %>
132
+ ```
133
+
134
+
135
+ The following is rendered when used `render AvatarComponent.new(initials: "SG", size: :small, border: true)`:
136
+
137
+ ```html
138
+ <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">
139
+ <span class="text-xs font-medium leading-none text-white">SG</span>
140
+ </div>
141
+ ```
142
+
143
+ 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"})`:
144
+
145
+ ```html
146
+ <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">
147
+ ```
148
+
149
+ ----
150
+
151
+ ![Example](examples/avatar.png)
152
+
153
+
154
+ ## Another ViewComponent + Vident example with Stimulus
155
+
156
+ Consider the following ERB that might be part of an application's views. The app uses `ViewComponent`, `Stimulus` and `Vident`.
157
+
158
+ The Greeter is a component that displays a text input and a button. When the button is clicked, the text input's value is
159
+ used to greet the user. At the same time the button changes to be a 'reset' button, which resets the greeting when clicked again.
160
+
161
+ ![ex1.gif](examples%2Fex1.gif)
162
+
163
+ ```erb
164
+ <%# app/views/home/index.html.erb %>
165
+
166
+ <!-- ... -->
167
+
168
+ <!-- render the Greeter ViewComponent (that uses Vident) -->
169
+ <%= render ::GreeterComponent.new(cta: "Hey!", html_options: {class: "my-4"}) do |greeter| %>
170
+ <%# this component has a slot called `trigger` that renders a `ButtonComponent` (which also uses Vident) %>
171
+ <% greeter.trigger(
172
+
173
+ # The button component has attributes that are typed
174
+ before_clicked: "Greet",
175
+ after_clicked: "Greeted! Reset?",
176
+
177
+ # A stimulus action is added to the button that triggers the `greet` action on the greeter stimulus controller.
178
+ # This action will be added to any defined on the button component itself
179
+ actions: [
180
+ greeter.action(:click, :greet),
181
+ ],
182
+
183
+ # We can also override the default button classes of our component, or set other HTML attributes
184
+ html_options: {
185
+ class: "bg-red-500 hover:bg-red-700"
186
+ }
187
+ ) %>
188
+ <% end %>
189
+
190
+ <!-- ... -->
191
+ ```
192
+
193
+ The output HTML of the above, using Vident, is:
194
+
195
+ ```html
196
+ <div class="greeter-component py-2 my-4"
197
+ data-controller="greeter-component"
198
+ data-greeter-component-pre-click-class="text-md text-gray-500"
199
+ data-greeter-component-post-click-class="text-xl text-blue-700"
200
+ id="greeter-component-1599855-6">
201
+ <input type="text"
202
+ data-greeter-component-target="name"
203
+ class="shadow appearance-none border rounded py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
204
+ <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"
205
+ data-controller="button-component"
206
+ data-action="click->greeter-component#greet button-component#changeMessage"
207
+ data-button-component-after-clicked-message="Greeted! Reset?"
208
+ data-button-component-before-clicked-message="Greet"
209
+ id="button-component-7799479-7">Hey!</button>
210
+ <!-- you can also use the `target_tag` helper to render targets -->
211
+ <span class="ml-4 text-md text-gray-500"
212
+ data-greeter-component-target="output">
213
+ ...
214
+ </span>
215
+ </div>
216
+ ```
217
+
218
+ Let's look at the components in more detail.
219
+
220
+ The main component is the `GreeterComponent`:
221
+
222
+ ```ruby
223
+ # app/components/greeter_component.rb
224
+
225
+ class GreeterComponent < ::Vident::ViewComponent::Base
226
+ renders_one :trigger, ButtonComponent
227
+ end
228
+ ```
229
+
230
+ ```erb
231
+ <%# app/components/greeter_component.html.erb %>
232
+
233
+ <%# Rendering the `root` element creates a tag which has stimulus `data-*`s, a unique id & other attributes set. %>
234
+ <%# The stimulus controller name (identifier) is derived from the component name, and then used to generate the relavent data attribute names. %>
235
+
236
+ <%= render root named_classes: {
237
+ pre_click: "text-md text-gray-500", # named classes are exposed to Stimulus as `data-<controller>-<name>-class` attributes
238
+ post_click: "text-xl text-blue-700",
239
+ html_options: {class: "py-2"}
240
+ } do |greeter| %>
241
+
242
+ <%# `greeter` is the root element and exposes methods to generate stimulus targets and actions %>
243
+ <input type="text"
244
+ <%= greeter.as_target(:name) %>
245
+ class="shadow appearance-none border rounded py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
246
+
247
+ <%# Render the slot %>
248
+ <%= trigger %>
249
+
250
+ <%# you can also use the `target_tag` helper to render targets %>
251
+ <%= greeter.target_tag(
252
+ :span,
253
+ :output,
254
+ # Stimulus named classes can be referenced to set class attributes at render time
255
+ class: "ml-4 #{greeter.named_classes(:pre_click)}"
256
+ ) do %>
257
+ ...
258
+ <% end %>
259
+ <% end %>
260
+
261
+ ```
262
+
263
+ ```js
264
+ // app/components/greeter_component_controller.js
265
+
266
+ import { Controller } from "@hotwired/stimulus"
267
+
268
+ // This is a Stimulus controller that is automatically registered for the `GreeterComponent`
269
+ // and is 'sidecar' to the component. You can see that while in the ERB we use Ruby naming conventions
270
+ // with snake_case Symbols, here they are converted to camelCase names. We can also just use camelCase
271
+ // in the ERB if we want.
272
+ export default class extends Controller {
273
+ static targets = [ "name", "output" ]
274
+ static classes = [ "preClick", "postClick" ]
275
+
276
+ greet() {
277
+ this.clicked = !this.clicked;
278
+ this.outputTarget.classList.toggle(this.preClickClasses, !this.clicked);
279
+ this.outputTarget.classList.toggle(this.postClickClasses, this.clicked);
280
+
281
+ if (this.clicked)
282
+ this.outputTarget.textContent = `Hello, ${this.nameTarget.value}!`
283
+ else
284
+ this.clear();
285
+ }
286
+
287
+ clear() {
288
+ this.outputTarget.textContent = '...';
289
+ this.nameTarget.value = '';
290
+ }
291
+ }
292
+ ```
293
+
294
+ The slot renders a `ButtonComponent` component:
295
+
296
+ ```ruby
297
+ # app/components/button_component.rb
298
+
299
+ class ButtonComponent < ::Vident::Typed::ViewComponent::Base
300
+ # The attributes can specify an expected type, a default value and if nil is allowed.
301
+ attribute :after_clicked, String, default: "Greeted!"
302
+ attribute :before_clicked, String, allow_nil: false
303
+
304
+ # This example is a templateless ViewComponent.
305
+ def call
306
+ # The button is rendered as a <button> tag with an click action on its own controller.
307
+ render root(
308
+ element_tag: :button,
309
+
310
+ # We can define actions as arrays of Symbols, or pass manually manually crafted strings.
311
+ # Here we specify the action name only, implying an action on the current components controller
312
+ # and the default event type of `click`.
313
+ actions: [:change_message],
314
+ # Alternatively: [:click, :change_message] or ["click", "changeMessage"] or even "click->button-component#changeMessage"
315
+
316
+ # A couple of data values are also set which will be available to the controller
317
+ data_maps: [{after_clicked_message: after_clicked, before_clicked_message: before_clicked}],
318
+
319
+ # The <button> tag has a default styling set directly on it. Note that
320
+ # if not using utility classes, you can style the component using its
321
+ # canonical class name (which is equal to the component's stimulus identifier),
322
+ # in this case `button-component`.
323
+ html_options: {class: "ml-4 whitespace-no-wrap bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"}
324
+ ) do
325
+ @before_clicked
326
+ end
327
+ end
328
+ end
329
+ ```
330
+
331
+ ```js
332
+ // app/components/button_component_controller.js
333
+
334
+ import { Controller } from "@hotwired/stimulus"
335
+
336
+ export default class extends Controller {
337
+ // The action is in camelCase.
338
+ changeMessage() {
339
+ this.clicked = !this.clicked;
340
+ // The data attributes have their naming convention converted to camelCase.
341
+ this.element.textContent = this.clicked ? this.data.get("afterClickedMessage") : this.data.get("beforeClickedMessage");
342
+ }
343
+ }
344
+
345
+ ```
346
+
347
+
348
+ ## Usage
349
+ How to use my plugin.
350
+
351
+ ## Installation
352
+ Add this line to your application's Gemfile:
353
+
354
+ ```ruby
355
+ gem "vident-typed-view_component"
356
+ ```
357
+
358
+ And then execute:
359
+ ```bash
360
+ $ bundle
361
+ ```
362
+
363
+ Or install it yourself as:
364
+ ```bash
365
+ $ gem install vident-typed-view_component
366
+ ```
367
+
368
+ ## Contributing
369
+ Contribution directions go here.
370
+
371
+ ## License
372
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ load "rails/tasks/statistics.rake"
7
+
8
+ require "bundler/gem_tasks"
data/config/routes.rb ADDED
@@ -0,0 +1,2 @@
1
+ Rails.application.routes.draw do
2
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :vident_typed_view_component do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,15 @@
1
+ module Vident
2
+ module Typed
3
+ module ViewComponent
4
+ class Base < ::ViewComponent::Base
5
+ include ::Vident::Typed::Component
6
+
7
+ # Helper to create the main element
8
+ def parent_element(**options)
9
+ @parent_element ||= ::Vident::ViewComponent::RootComponent.new(**parent_element_attributes(options))
10
+ end
11
+ alias_method :root, :parent_element
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,19 @@
1
+ module Vident
2
+ module Typed
3
+ module ViewComponent
4
+ class Engine < ::Rails::Engine
5
+ lib_path = File.expand_path("../../../../../lib/", __FILE__)
6
+ config.autoload_paths << lib_path
7
+ config.eager_load_paths << lib_path
8
+
9
+ config.before_initialize do
10
+ Rails.autoloaders.each do |autoloader|
11
+ autoloader.inflector.inflect(
12
+ "version" => "VERSION"
13
+ )
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,7 @@
1
+ module Vident
2
+ module Typed
3
+ module ViewComponent
4
+ VERSION = "0.1.0"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,10 @@
1
+ require "vident/typed/view_component/version"
2
+ require "vident/typed/view_component/engine"
3
+
4
+ module Vident
5
+ module Typed
6
+ module ViewComponent
7
+ # Your code goes here...
8
+ end
9
+ end
10
+ end
metadata ADDED
@@ -0,0 +1,114 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: vident-typed-view_component
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Stephen Ierodiaconou
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-04-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '7'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '8'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '7'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '8'
33
+ - !ruby/object:Gem::Dependency
34
+ name: vident-typed
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: 0.1.0
40
+ - - "<"
41
+ - !ruby/object:Gem::Version
42
+ version: '1'
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: 0.1.0
50
+ - - "<"
51
+ - !ruby/object:Gem::Version
52
+ version: '1'
53
+ - !ruby/object:Gem::Dependency
54
+ name: vident-view_component
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: 0.1.0
60
+ - - "<"
61
+ - !ruby/object:Gem::Version
62
+ version: '1'
63
+ type: :runtime
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: 0.1.0
70
+ - - "<"
71
+ - !ruby/object:Gem::Version
72
+ version: '1'
73
+ description: Vident with ViewComponent & typed attributes
74
+ email:
75
+ - stevegeek@gmail.com
76
+ executables: []
77
+ extensions: []
78
+ extra_rdoc_files: []
79
+ files:
80
+ - README.md
81
+ - Rakefile
82
+ - config/routes.rb
83
+ - lib/tasks/vident/typed/view_component_tasks.rake
84
+ - lib/vident/typed/view_component.rb
85
+ - lib/vident/typed/view_component/base.rb
86
+ - lib/vident/typed/view_component/engine.rb
87
+ - lib/vident/typed/view_component/version.rb
88
+ homepage: https://github.com/stevegeek/vident-typed-view_component
89
+ licenses:
90
+ - MIT
91
+ metadata:
92
+ homepage_uri: https://github.com/stevegeek/vident-typed-view_component
93
+ source_code_uri: https://github.com/stevegeek/vident-typed-view_component
94
+ changelog_uri: https://github.com/stevegeek/vident-typed-view_component/blob/main/CHANGELOG.md
95
+ post_install_message:
96
+ rdoc_options: []
97
+ require_paths:
98
+ - lib
99
+ required_ruby_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ required_rubygems_version: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: '0'
109
+ requirements: []
110
+ rubygems_version: 3.4.6
111
+ signing_key:
112
+ specification_version: 4
113
+ summary: Vident with ViewComponent & typed attributes
114
+ test_files: []