stimulus_builder-rails 0.1.0.alpha.pre.1

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: 5cdeb6e857f28a15b6fe3bdd7051410db4131ae29a8749672400645c788794bf
4
+ data.tar.gz: fa3d4a4cbeb560f81c463883f42fc4f5f0615084c509cf1941f2657f6b35e1da
5
+ SHA512:
6
+ metadata.gz: 9a4479b2f7bd616293bb3680dd3d68b1d450d99259cdbe4855ca1fde04c05a30cda8b103c043e012f1a0ae75f73eedbe531eef74fb333ec384d6880c38d4871e
7
+ data.tar.gz: 06febb06ac7d6515e1c502565168023b85b827526fd9ef82da8dbb3fea2d688beca87395c24525d01386f577385548d436c1f160609faee03d36972384a510df
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2024 Nipun Paradkar
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,408 @@
1
+ # StimulusBuilder
2
+
3
+ Manually adding Stimulus Attributes to HTML elements was something that I didn't like because:
4
+
5
+ - they were hard to notice and track in the view files
6
+ - had to write them manually without any structure
7
+ - didn't look good clubbed with other attributes
8
+
9
+ So, I decided to write this gem to provide a syntax that translates to the Stimulus Attributes behind the scene.
10
+
11
+ ## Usage
12
+
13
+ The gem takes inspiration from `ActionView::Helpers::FormBuilder` and tries to follow the syntax set by it.
14
+
15
+ ### Connecting controllers
16
+
17
+ ```erb
18
+ <%= stimulated.div do |component| %>
19
+ <% component.connect(:reference) %>
20
+ <% end %>
21
+ ```
22
+
23
+ will output:
24
+
25
+ ```html
26
+ <div data-controller="reference"></div>
27
+ ```
28
+
29
+ The `#connect` method accepts strings as well:
30
+
31
+ ```erb
32
+ <% component.connect("clipboard") %>
33
+ ```
34
+
35
+ Using strings, you can also specify a namespaced controller:
36
+
37
+ ```erb
38
+ <%= stimulated.div do |component| %>
39
+ <% component.connect("users/list_item") %>
40
+ <% end %>
41
+ ```
42
+
43
+ which will output:
44
+
45
+ ```html
46
+ <div data-controller="users--list-item"></div>
47
+ ```
48
+
49
+ If your controller consists of multiple words, you can use `snake_case` notation via symbols:
50
+
51
+ ```erb
52
+ <%= stimulated.div do |component| %>
53
+ <% component.connect(:date_picker) %>
54
+ <% end %>
55
+ ```
56
+
57
+ or via strings:
58
+
59
+ ```erb
60
+ <%= stimulated.div do |component| %>
61
+ <% component.connect("date_picker") %>
62
+ <% end %>
63
+ ```
64
+
65
+ both will output:
66
+
67
+ ```html
68
+ <div data-controller="date-picker"></div>
69
+ ```
70
+
71
+ #### Connecting multiple controllers
72
+
73
+ ```erb
74
+ <%= stimulated.div do |component| %>
75
+ <% component.connect(:clipboard) %>
76
+ <% component.connect(:list_item) %>
77
+ <% end %>
78
+ ```
79
+
80
+ will output:
81
+
82
+ ```html
83
+ <div data-controller="clipboard list-item"></div>
84
+ ```
85
+
86
+ ### Attaching actions
87
+
88
+ ```erb
89
+ <%= stimulated.div do |component| %>
90
+ <% gallery = component.connect(:gallery) %>
91
+
92
+ <%= stimulated.button do |button| %>
93
+ <% button.on("click") { gallery.next } %>
94
+ <% end %>
95
+ <% end %>
96
+ ```
97
+
98
+ will output:
99
+
100
+ ```html
101
+ <div data-controller="gallery">
102
+ <button data-action="click->gallery#next"></button>
103
+ </div>
104
+ ```
105
+
106
+ The `#connect` method returns a representation of the controller that's passed to it. Calling methods on this object inside the block passed to `#on`, and also passing the event will convert it to an action attribute.
107
+
108
+ #### Event shorthand
109
+
110
+ If you want to fallback to the default event, use the `#fire` method instead of `#on`:
111
+
112
+ ```erb
113
+ <%= stimulated.button do |button| %>
114
+ <% button.fire { gallery.next } %>
115
+ <% end %>
116
+ ```
117
+
118
+ will generate:
119
+
120
+ ```html
121
+ <button data-action="gallery#next"></button>
122
+ ```
123
+
124
+ #### Global events
125
+
126
+ The second parameter to `#on` is where the event should be attached. It can either be `:window`, or `:document`:
127
+
128
+ ```erb
129
+ <%= stimulated.div do |component| %>
130
+ <% gallery = component.connect(:gallery) %>
131
+
132
+ <% component.on("resize", :window) { gallery.layout } %>
133
+ <% end %>
134
+ ```
135
+
136
+ will output:
137
+
138
+ ```html
139
+ <div data-controller="gallery" data-action="resize@window->gallery#layout"></div>
140
+ ```
141
+
142
+ #### Action options
143
+
144
+ Action options can be passed via hash parameters to `#on`:
145
+
146
+ ```erb
147
+ <%= stimulated.div do |component| %>
148
+ <% gallery = component.connect(:gallery) %>
149
+
150
+ <% component.on("scroll", passive: false) { gallery.layout } %>
151
+
152
+ <%= stimulated.img do |image| %>
153
+ <% image.on("click", capture: true) { gallery.open }
154
+ <% end %>
155
+ <% end %>
156
+ ```
157
+
158
+ will output:
159
+
160
+ ```html
161
+ <div data-controller="gallery" data-action="scroll->gallery#layout:!passive">
162
+ <img data-action="click->gallery#open:capture">
163
+ </div>
164
+ ```
165
+
166
+ #### Multiple actions
167
+
168
+ Calling `#on` more than once will append actions to the action attribute:
169
+
170
+ ```erb
171
+ <%= stimulated.div do |component| %>
172
+ <% field = component.connect(:field) %>
173
+ <% search = component.connect(:search) %>
174
+
175
+ <%= stimulated.input(type: "text") do |input| %>
176
+ <% input.on("focus") { field.highlight } %>
177
+ <% input.on("input") { search.update } %>
178
+ <% end %>
179
+ <% end
180
+ ```
181
+
182
+ will output:
183
+
184
+ ```html
185
+ <div data-controller="field search">
186
+ <input type="text" data-action="focus->field#highlight input->search#update">
187
+ </div>
188
+ ```
189
+
190
+ #### Naming conventions
191
+
192
+ If the method calls on the controller representation object is more than one word, it'll `camelCase` it:
193
+
194
+ ```erb
195
+ <%= stimulated.div do |component| %>
196
+ <% profile = component.connect(:profile) %>
197
+
198
+ <%= stimulated.button do |input| %>
199
+ <% input.on("click") { profile.show_dialog } %>
200
+ <% end %>
201
+ <% end
202
+ ```
203
+
204
+ will output:
205
+
206
+ ```html
207
+ <div data-controller="profile">
208
+ <button data-action="click->profile#showDialog">
209
+ </div>
210
+ ```
211
+
212
+ #### Action parameters
213
+
214
+ ```erb
215
+ <%= stimulated.div do |component| %>
216
+ <% item = component.connect(:item) %>
217
+ <% spinner = component.connect(:spinner) %>
218
+
219
+ <%= stimulated.button do |input| %>
220
+ <% input.fire do %>
221
+ <% item.upvote(id: "12345", url: "/votes", active: true) %>
222
+ <% end %>
223
+ <% input.fire { spinner.start } %>
224
+ <% end %>
225
+ <% end
226
+ ```
227
+
228
+ will output:
229
+
230
+ ```html
231
+ <div data-controller="item spinner">
232
+ <button data-action="item#upvote spinner#start"
233
+ data-item-id-param="12345"
234
+ data-item-url-param="/votes"
235
+ data-item-active-param="true">
236
+ </button>
237
+ </div>
238
+ ```
239
+
240
+ ### Referencing targets
241
+
242
+ The syntax to an element as a target for a controller is `[controller].[target_name] = [element]`.
243
+
244
+ For example:
245
+
246
+ ```erb
247
+ <%= stimulated.div do |component| %>
248
+ <% search = component.connect(:search) %>
249
+
250
+ <%= stimulated.input(type: "text") do |input| %>
251
+ <% search.query = input %>
252
+ <% end %>
253
+
254
+ <%= stimulated.div do |element| %>
255
+ <% search.error_message = element %>
256
+ <% end %>
257
+
258
+ <%= stimulated.div do |element| %>
259
+ <% search.results = element %>
260
+ <% end %>
261
+ <% end %>
262
+ ```
263
+
264
+ will output:
265
+
266
+ ```html
267
+ <div data-controller="search">
268
+ <input type="text" data-search-target="query">
269
+ <div data-search-target="errorMessage"></div>
270
+ <div data-search-target="results"></div>
271
+ </div>
272
+ ```
273
+
274
+ #### Shared targets
275
+
276
+ You can add same element as a target for different controllers:
277
+
278
+ ```erb
279
+ <%= stimulated.form_for(:users, url: "/users") do |form| %>
280
+ <% search = form.connect(:search) %>
281
+ <% checkbox = form.connect(:checkbox) %>
282
+
283
+ <%= stimulated.check_box do |input| %>
284
+ <% search.projects = input %>
285
+ <% checkbox.input = input %>
286
+ <% end %>
287
+
288
+ <%= stimulated.check_box do |input| %>
289
+ <% search.messages = input %>
290
+ <% checkbox.input = input %>
291
+ <% end %>
292
+ <% end %>
293
+ ```
294
+
295
+ will output:
296
+
297
+ ```html
298
+ <form action="/users" accept-charset="UTF-8" method="post" data-controller="search checkbox">
299
+ <input type="checkbox" data-search-target="projects" data-checkbox-target="input">
300
+ <input type="checkbox" data-search-target="messages" data-checkbox-target="input">
301
+ </form>
302
+ ```
303
+
304
+ #### Naming conventions
305
+
306
+ When target names are more than one word, use `snake_case` for the method names on the `[controller]`:
307
+
308
+ ```erb
309
+ <%= stimulated.div do |component| %>
310
+ <% search = component.connect(:search) %>
311
+
312
+ <% stimulated.span do |element| %>
313
+ <% search.snake_case = element %>
314
+ <% end %>
315
+ <% end %>
316
+ ```
317
+
318
+ ### Outlets
319
+
320
+ The `#use` method present on the element returns an object that is a logical representation of an outlet controller. We can then assign this outlet object to the controller where this outlet needs to be accessible by using the controller's `#[]=` method.
321
+
322
+ ```erb
323
+ <div>
324
+ <%= stimulated.div(class: "online-user") do |component| %>
325
+ <% component.connect(:user_status)
326
+ <% end %>
327
+ <%= stimulated.div(class: "online-user") do |component| %>
328
+ <% component.connect(:user_status)
329
+ <% end %>
330
+ <%# ... %>
331
+ </div>
332
+
333
+ <%# ... %>
334
+
335
+ <%= stimulated.div(id: "chat") do |component| %>
336
+ <% chat = component.connect(:chat) %>
337
+ <% user_status = component.use(:user_status) %>
338
+
339
+ <% chat[".online-user"] = user_status %>
340
+ <% end %>
341
+ ```
342
+
343
+ The above will add a `data-chat-user-status-outlet=".online-user"` attribute on the `div#chat` element.
344
+
345
+ `#use` accepts a name of the controller to be used as an outlet, and it has the same naming convention rules as the `#connect` method. The only difference between the both is that the controller returned by the `#use` method is simpler than the one returned by the `#connect` method, _i.e._ it cannot be used to do anything else like setting up actions, or adding targets, etc.
346
+
347
+ ### Values
348
+
349
+ You can pass values to the controller by passing a hash as the second parameter to `#connect`. This hash needs to have the `:values` key present, containing all the values.
350
+
351
+ ```html
352
+ <%= stimulated.div do |component| %>
353
+ <%
354
+ component.connect(:loader, {
355
+ values: { url: "/messages" },
356
+ })
357
+ %>
358
+ <% end %>
359
+ ```
360
+
361
+ will output:
362
+
363
+ ```html
364
+ <div data-controller="loader" data-loader-url-value="/messages"></div>
365
+ ```
366
+
367
+ ### Classes
368
+
369
+ You can pass classes (Stimulus classes, not HTML classes) to the controller by passing a hash as the second parameter to `#connect`. This hash needs to have the `:classes` key present, containing all the values.
370
+
371
+ ```html
372
+ <%= stimulated.form_for(:user, url: "/users") do |form| %>
373
+ <%
374
+ form.connect(:search, {
375
+ classes: { loading: "search--busy" },
376
+ })
377
+ %>
378
+ <% end %>
379
+ ```
380
+
381
+ will output:
382
+
383
+ ```html
384
+ <form action="/users" data-controller="search" data-search-loading-class="search--busy"></form>
385
+ ```
386
+
387
+ ## Installation
388
+ Add this line to your application's Gemfile:
389
+
390
+ ```ruby
391
+ gem "stimulus_builder"
392
+ ```
393
+
394
+ And then execute:
395
+ ```bash
396
+ $ bundle
397
+ ```
398
+
399
+ Or install it yourself as:
400
+ ```bash
401
+ $ gem install stimulus_builder
402
+ ```
403
+
404
+ ## Contributing
405
+ Contribution directions go here.
406
+
407
+ ## License
408
+ 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,3 @@
1
+ require "bundler/setup"
2
+
3
+ require "bundler/gem_tasks"
@@ -0,0 +1,27 @@
1
+ module StimulusBuilder
2
+ class ActionAttribute < Attribute
3
+ def initialize(*action_descriptors)
4
+ @action_descriptors = action_descriptors
5
+ end
6
+
7
+ def name
8
+ "data-action"
9
+ end
10
+
11
+ def value
12
+ @action_descriptors.join(" ").html_safe
13
+ end
14
+
15
+ def +(action_attribute)
16
+ self.class.new(*(@action_descriptors + action_attribute.action_descriptors))
17
+ end
18
+
19
+ def multi?
20
+ true
21
+ end
22
+
23
+ protected
24
+
25
+ attr_reader :action_descriptors
26
+ end
27
+ end
@@ -0,0 +1,38 @@
1
+ module StimulusBuilder
2
+ class ActionDescriptor
3
+ def initialize(event, handler, target = nil, **options)
4
+ @event, @handler = event, handler
5
+ @target, @options = target, options
6
+ end
7
+
8
+ def to_s
9
+ descriptor = ""
10
+
11
+ if @event.present?
12
+ descriptor += @event
13
+
14
+ if @target.present?
15
+ descriptor += "@#{@target}"
16
+ end
17
+
18
+ descriptor += "->"
19
+ end
20
+
21
+
22
+ descriptor + "#{@handler}" + processed_options
23
+ end
24
+
25
+ private
26
+
27
+ def processed_options
28
+ @options.inject("") do |processed_options, (option, condition)|
29
+ processed_options +=
30
+ if condition == true
31
+ ":#{option}"
32
+ elsif condition == false
33
+ ":!#{option}"
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,11 @@
1
+ module StimulusBuilder
2
+ class Attribute
3
+ def to_hash
4
+ { name => value }
5
+ end
6
+
7
+ def multi?
8
+ false
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,16 @@
1
+ module StimulusBuilder
2
+ class ClassAttribute < Attribute
3
+ def initialize(identifier, logical_name, klass)
4
+ @identifier = identifier
5
+ @logical_name, @klass = logical_name.to_s, klass
6
+ end
7
+
8
+ def name
9
+ "data-#{@identifier}-#{@logical_name.dasherize}-class"
10
+ end
11
+
12
+ def value
13
+ @klass
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,47 @@
1
+ module StimulusBuilder
2
+ class Controller
3
+ MODULE_SEPARATOR = "/".freeze
4
+ IDENTIFIER_SEPARATOR = "--".freeze
5
+
6
+ private_constant :MODULE_SEPARATOR, :IDENTIFIER_SEPARATOR
7
+
8
+ def initialize(controller_name, element)
9
+ @controller_name = controller_name.to_s
10
+ @element = element
11
+ end
12
+
13
+ def method_missing(action_method, *args)
14
+ if action_method.ends_with?("=".freeze)
15
+ args[0] << TargetAttribute.new(self, action_method[..-2])
16
+ else
17
+ params = args[0] || {}
18
+ Handler.new(self, action_method, params)
19
+ end
20
+ end
21
+
22
+ def [](event_name)
23
+ "#{self}:#{event_name.to_s.dasherize}"
24
+ end
25
+
26
+ def []=(selector, outlet)
27
+ @element << OutletAttribute.new(self, outlet, selector)
28
+ end
29
+
30
+ def to_s
31
+ to_str
32
+ end
33
+
34
+ def to_str
35
+ controller_identifier
36
+ end
37
+
38
+ private
39
+
40
+ def controller_identifier
41
+ @controller_name
42
+ .split(MODULE_SEPARATOR)
43
+ .map(&:dasherize)
44
+ .join(IDENTIFIER_SEPARATOR)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,27 @@
1
+ module StimulusBuilder
2
+ class ControllerAttribute < Attribute
3
+ def initialize(*controllers)
4
+ @controllers = controllers
5
+ end
6
+
7
+ def name
8
+ "data-controller"
9
+ end
10
+
11
+ def value
12
+ @controllers.join(" ")
13
+ end
14
+
15
+ def +(controller_attribute)
16
+ self.class.new(*(@controllers + controller_attribute.controllers))
17
+ end
18
+
19
+ def multi?
20
+ true
21
+ end
22
+
23
+ protected
24
+
25
+ attr_reader :controllers
26
+ end
27
+ end
@@ -0,0 +1,14 @@
1
+ module StimulusBuilder
2
+ class Element
3
+ include ElementRepresentable
4
+
5
+ def initialize
6
+ @attributes = []
7
+ end
8
+
9
+ # FIXME: This is needed to not render this element in tests.
10
+ def to_s
11
+ ''.freeze
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,78 @@
1
+ module StimulusBuilder
2
+ module ElementRepresentable
3
+ def attributes
4
+ @attributes.inject({}) do |memo, attribute|
5
+ memo.merge(attribute)
6
+ end
7
+ end
8
+
9
+ def <<(attribute)
10
+ update_attributes!(attribute)
11
+ end
12
+
13
+ def connect(identifier_name, props = nil)
14
+ Controller.new(identifier_name, self).tap do |controller|
15
+ self << ControllerAttribute.new(controller)
16
+
17
+ unless props.nil?
18
+ create_value_attributes!(controller, props[:values]) if props[:values].present?
19
+ create_class_attributes!(controller, props[:classes]) if props[:classes].present?
20
+ end
21
+ end
22
+ end
23
+
24
+ def use(controller_name)
25
+ Outlet.new(controller_name)
26
+ end
27
+
28
+ def fire(**options, &block)
29
+ on(nil, **options, &block)
30
+ end
31
+
32
+ def on(event, at = nil, **options, &block)
33
+ block.call.then do |handler|
34
+ self << ActionAttribute.new(ActionDescriptor.new(event, handler, at, **options))
35
+
36
+ handler.param_attributes.each do |param_attribute|
37
+ self << param_attribute
38
+ end
39
+ end
40
+
41
+ # FIXME: This is required so that when this method is called from Ruby files,
42
+ # it doesn't output the value that gets returned by the above line.
43
+ ''
44
+ end
45
+
46
+ private
47
+
48
+ def create_value_attributes!(identifier, value_props)
49
+ value_props.each do |value_name, value_value|
50
+ self << ValueAttribute.new(identifier, value_name, value_value)
51
+ end
52
+ end
53
+
54
+ def create_class_attributes!(identifier, class_props)
55
+ class_props.each do |class_name, class_value|
56
+ self << ClassAttribute.new(identifier, class_name, class_value)
57
+ end
58
+ end
59
+
60
+ def update_attributes!(attribute)
61
+ if attribute.multi?
62
+ unless (current_index = attribute_index(attribute)).nil?
63
+ @attributes[current_index] += attribute
64
+ else
65
+ @attributes << attribute
66
+ end
67
+ else
68
+ @attributes << attribute
69
+ end
70
+ end
71
+
72
+ def attribute_index(attribute)
73
+ @attributes.index do |iterable_attribute|
74
+ attribute.name == iterable_attribute.name
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,36 @@
1
+ module StimulusBuilder
2
+ class FormBuilder < ActionView::Helpers::FormBuilder
3
+ include ElementRepresentable
4
+
5
+ def initialize(object_name, object, template, options)
6
+ @attributes = []
7
+
8
+ super(object_name, object, template, options)
9
+ end
10
+
11
+ def <<(attribute)
12
+ super(attribute)
13
+
14
+ options[:html] ||= {}
15
+ options[:html].merge!(attributes)
16
+ end
17
+
18
+ def check_box(method, options = {}, checked_value = "1", unchecked_value = "0")
19
+ Element.new.then do |element|
20
+ yield(element)
21
+
22
+ super(method, options.merge(element.attributes), checked_value, unchecked_value)
23
+ end
24
+ end
25
+
26
+ def button(value = nil, options = {})
27
+ Element.new.then do |element|
28
+ @template.capture do
29
+ yield(element, value)
30
+ end.then do |inner_html|
31
+ super(value, options.merge(element.attributes)) { inner_html }
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,19 @@
1
+ module StimulusBuilder
2
+ class Handler
3
+ def initialize(controller, action_method, params)
4
+ @controller = controller
5
+ @action_method = action_method.to_s
6
+ @params = params
7
+ end
8
+
9
+ def param_attributes
10
+ @params.map do |param_name, param_value|
11
+ ParamAttribute.new(@controller, param_name, param_value)
12
+ end
13
+ end
14
+
15
+ def to_s
16
+ "#{@controller}##{@action_method.camelize(:lower)}"
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,7 @@
1
+ module StimulusBuilder
2
+ module Helper
3
+ def stimulated
4
+ HelperDelegate.new(self)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,37 @@
1
+ module StimulusBuilder
2
+ class HelperDelegate
3
+ ELEMENT_NAMES = [
4
+ :div, :span, :input, :button, :img
5
+ ].freeze
6
+
7
+ private_constant :ELEMENT_NAMES
8
+
9
+ def initialize(view_context)
10
+ @view_context = view_context
11
+ end
12
+
13
+ def form_for(record, options = {}, &block)
14
+ options[:builder] ||= FormBuilder
15
+
16
+ @view_context.form_for(record, options, &block)
17
+ end
18
+
19
+ ELEMENT_NAMES.each do |element_name|
20
+ define_method(element_name) do |content = nil, **options, &block|
21
+ Element.new.then do |element|
22
+ @view_context.capture do
23
+ block.call(element)
24
+ end.then do |inner_html|
25
+ @view_context
26
+ .tag
27
+ .public_send(
28
+ element_name,
29
+ inner_html,
30
+ **options.merge(element.attributes)
31
+ )
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,23 @@
1
+ class StimulusBuilder::Outlet
2
+ MODULE_SEPARATOR = "/".freeze
3
+ IDENTIFIER_SEPARATOR = "--".freeze
4
+
5
+ private_constant :MODULE_SEPARATOR, :IDENTIFIER_SEPARATOR
6
+
7
+ def initialize(name)
8
+ @name = name.to_s
9
+ end
10
+
11
+ def to_s
12
+ identifier
13
+ end
14
+
15
+ private
16
+
17
+ def identifier
18
+ @name
19
+ .split(MODULE_SEPARATOR)
20
+ .map(&:dasherize)
21
+ .join(IDENTIFIER_SEPARATOR)
22
+ end
23
+ end
@@ -0,0 +1,17 @@
1
+ module StimulusBuilder
2
+ class OutletAttribute < Attribute
3
+ def initialize(identifier, outlet, selector)
4
+ @identifier = identifier
5
+ @outlet = outlet
6
+ @selector = selector
7
+ end
8
+
9
+ def name
10
+ "data-#{@identifier}-#{@outlet}-outlet"
11
+ end
12
+
13
+ def value
14
+ @selector
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,24 @@
1
+ module StimulusBuilder
2
+ class ParamAttribute < Attribute
3
+ attr_reader :value
4
+
5
+ def initialize(identifier, name, value)
6
+ @identifier = identifier
7
+ @name, @value = name.to_s, value
8
+ end
9
+
10
+ def name
11
+ "data-#{@identifier}-#{dasherized_name}-param"
12
+ end
13
+
14
+ def to_hash
15
+ { name => value }
16
+ end
17
+
18
+ private
19
+
20
+ def dasherized_name
21
+ @name.dasherize
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,9 @@
1
+ require "stimulus_builder/helper"
2
+
3
+ module StimulusBuilder
4
+ class Railtie < ::Rails::Railtie
5
+ initializer "stimulus_builder.helper" do
6
+ ActionView::Base.send :include, StimulusBuilder::Helper
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,36 @@
1
+ module StimulusBuilder
2
+ class TargetAttribute < Attribute
3
+ def initialize(identifier, *target_names)
4
+ @identifier = identifier
5
+ @target_names = target_names
6
+ end
7
+
8
+ def name
9
+ "data-#{@identifier}-target"
10
+ end
11
+
12
+ def value
13
+ properties.join(" ")
14
+ end
15
+
16
+ def multi?
17
+ true
18
+ end
19
+
20
+ def +(target_attribute)
21
+ self.class.new(@identifier, *(@target_names + target_attribute.target_names))
22
+ end
23
+
24
+ protected
25
+
26
+ attr_reader :target_names
27
+
28
+ private
29
+
30
+ def properties
31
+ @target_names.map do |target_name|
32
+ target_name.camelize(:lower)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,16 @@
1
+ module StimulusBuilder
2
+ class ValueAttribute < Attribute
3
+ def initialize(identifier, key, value)
4
+ @identifier = identifier
5
+ @key, @value = key.to_s, value
6
+ end
7
+
8
+ def name
9
+ "data-#{@identifier}-#{@key.dasherize}-value"
10
+ end
11
+
12
+ def value
13
+ @value
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,3 @@
1
+ module StimulusBuilder
2
+ VERSION = "0.1.0.alpha-1"
3
+ end
@@ -0,0 +1,22 @@
1
+ require "stimulus_builder/version"
2
+
3
+ require "stimulus_builder/attribute"
4
+ require "stimulus_builder/action_attribute"
5
+ require "stimulus_builder/action_descriptor"
6
+ require "stimulus_builder/class_attribute"
7
+ require "stimulus_builder/controller_attribute"
8
+ require "stimulus_builder/element_representable"
9
+ require "stimulus_builder/form_builder"
10
+ require "stimulus_builder/handler"
11
+ require "stimulus_builder/helper_delegate"
12
+ require "stimulus_builder/outlet"
13
+ require "stimulus_builder/outlet_attribute"
14
+ require "stimulus_builder/param_attribute"
15
+ require "stimulus_builder/target_attribute"
16
+ require "stimulus_builder/value_attribute"
17
+
18
+ require "stimulus_builder/railtie"
19
+
20
+ module StimulusBuilder
21
+ # Your code goes here...
22
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :stimulus_builder do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: stimulus_builder-rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0.alpha.pre.1
5
+ platform: ruby
6
+ authors:
7
+ - Nipun Paradkar
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-07-21 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.0.3.1
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 7.0.3.1
27
+ description:
28
+ email:
29
+ - nipunparadkar123@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - MIT-LICENSE
35
+ - README.md
36
+ - Rakefile
37
+ - lib/stimulus_builder.rb
38
+ - lib/stimulus_builder/action_attribute.rb
39
+ - lib/stimulus_builder/action_descriptor.rb
40
+ - lib/stimulus_builder/attribute.rb
41
+ - lib/stimulus_builder/class_attribute.rb
42
+ - lib/stimulus_builder/controller.rb
43
+ - lib/stimulus_builder/controller_attribute.rb
44
+ - lib/stimulus_builder/element.rb
45
+ - lib/stimulus_builder/element_representable.rb
46
+ - lib/stimulus_builder/form_builder.rb
47
+ - lib/stimulus_builder/handler.rb
48
+ - lib/stimulus_builder/helper.rb
49
+ - lib/stimulus_builder/helper_delegate.rb
50
+ - lib/stimulus_builder/outlet.rb
51
+ - lib/stimulus_builder/outlet_attribute.rb
52
+ - lib/stimulus_builder/param_attribute.rb
53
+ - lib/stimulus_builder/railtie.rb
54
+ - lib/stimulus_builder/target_attribute.rb
55
+ - lib/stimulus_builder/value_attribute.rb
56
+ - lib/stimulus_builder/version.rb
57
+ - lib/tasks/stimulus_builder_tasks.rake
58
+ homepage: https://github.com/radiantshaw/stimulus_builder-rails
59
+ licenses:
60
+ - MIT
61
+ metadata:
62
+ homepage_uri: https://github.com/radiantshaw/stimulus_builder-rails
63
+ source_code_uri: https://github.com/radiantshaw/stimulus_builder-rails
64
+ post_install_message:
65
+ rdoc_options: []
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">"
76
+ - !ruby/object:Gem::Version
77
+ version: 1.3.1
78
+ requirements: []
79
+ rubygems_version: 3.3.3
80
+ signing_key:
81
+ specification_version: 4
82
+ summary: Add Stimulus attributes using a nicer Ruby syntax.
83
+ test_files: []