vident-typed-view_component 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []