spark_components 1.0.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: 635a89fb74020c44aa354d88e39a317f09747b545b2f04a8a47091ba65e823e4
4
+ data.tar.gz: eb77464a1a07270e1fabfef42bba3cd121790c92e01a9322203292cf266745cb
5
+ SHA512:
6
+ metadata.gz: 9f6b10c0bef2d089d14ace1b0892db52d59a47b7999fa0e902f82d6158328ac61599e6890d34d221d86d04d400f8ad7048516e561b7ffb4651a6513edee43d88
7
+ data.tar.gz: 45e75e4a5ef27ca91fbc0de782541f19fa8fe3b10807eb9b9d5eb6dbb48fb87b08bcfee02e70278bbc4cc4d5e3e44a201b00558563a3660d10576dba91f2534f
data/MIT-LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ Original Work: Copyright 2018 Jens Ljungblad
2
+ Forked and Modifed: Copyright 2019 Brandon Mathis
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining
5
+ a copy of this software and associated documentation files (the
6
+ "Software"), to deal in the Software without restriction, including
7
+ without limitation the rights to use, copy, modify, merge, publish,
8
+ distribute, sublicense, and/or sell copies of the Software, and to
9
+ permit persons to whom the Software is furnished to do so, subject to
10
+ the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,524 @@
1
+ # Spark Components
2
+
3
+ Simple view components for Rails 5.1+, designed to go well with [styleguide](https://github.com/jensljungblad/styleguide). The two together are inspired by the works of [Brad Frost](http://bradfrost.com) and by the [thoughts behind](http://engineering.lonelyplanet.com/2014/05/18/a-maintainable-styleguide.html) Lonely Planet's style guide [Rizzo](http://rizzo.lonelyplanet.com).
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem "spark_components"
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ ```sh
16
+ $ bundle
17
+ ```
18
+
19
+ ## Components
20
+
21
+ The examples provided here will use the [BEM naming conventions](http://getbem.com/naming/).
22
+
23
+ Components live in `app/components`. Generate a component by executing:
24
+
25
+ ```sh
26
+ $ bin/rails g components:component alert
27
+ ```
28
+
29
+ This will create the following files:
30
+
31
+ ```
32
+ app/
33
+ components/
34
+ alert/
35
+ _alert.html.erb
36
+ alert.css
37
+ alert.js
38
+ alert_component.rb
39
+ ```
40
+
41
+ The generator also takes `--skip-css` and `--skip-js` options.
42
+
43
+ Let's add some markup and CSS:
44
+
45
+ ```erb
46
+ <% # app/components/alert/_alert.html.erb %>
47
+
48
+ <div class="alert alert--primary" role="alert">
49
+ Message
50
+ </div>
51
+ ```
52
+
53
+ ```css
54
+ /* app/components/alert/alert.css */
55
+
56
+ .alert {
57
+ padding: 1rem;
58
+ }
59
+
60
+ .alert--primary {
61
+ background: blue;
62
+ }
63
+
64
+ .alert--success {
65
+ background: green;
66
+ }
67
+
68
+ .alert--danger {
69
+ background: red;
70
+ }
71
+ ```
72
+
73
+ This component can now be rendered using the `component` helper:
74
+
75
+ ```erb
76
+ <%= component "alert" %>
77
+ ```
78
+
79
+ ### Assets
80
+
81
+ In order to require assets such as CSS, either require them manually in the manifest, e.g. `application.css`:
82
+
83
+ ```css
84
+ /*
85
+ *= require alert/alert
86
+ */
87
+ ```
88
+
89
+ Or require `components`, which will in turn require the assets for all components:
90
+
91
+ ```css
92
+ /*
93
+ *= require components
94
+ */
95
+ ```
96
+
97
+ ### Attributes and blocks
98
+
99
+ There are two ways of passing data to components: attributes and blocks. Attributes are useful for data such as ids, modifiers and data structures (models etc). Blocks are useful when you need to inject HTML content into components.
100
+
101
+ Let's define some attributes for the component we just created:
102
+
103
+ ```ruby
104
+ # app/components/alert_component.rb %>
105
+
106
+ class AlertComponent < SparkComponents::Component
107
+ attribute :context, :message
108
+ end
109
+ ```
110
+
111
+ ```erb
112
+ <% # app/components/alert/_alert.html.erb %>
113
+
114
+ <div class="alert alert--<%= alert.context %>" role="alert">
115
+ <%= alert.message %>
116
+ </div>
117
+ ```
118
+
119
+ ```erb
120
+ <%= component "alert", message: "Something went right!", context: "success" %>
121
+ <%= component "alert", message: "Something went wrong!", context: "danger" %>
122
+ ```
123
+
124
+ To inject some text or HTML content into our component we can print the component variable in our template, and populate it by passing a block to the component helper:
125
+
126
+ ```erb
127
+ <% # app/components/alert/_alert.html.erb %>
128
+
129
+ <div class="alert alert--<%= alert.context %>" role="alert">
130
+ <%= alert %>
131
+ </div>
132
+ ```
133
+
134
+ ```erb
135
+ <%= component "alert", context: "success" do %>
136
+ <em>Something</em> went right!
137
+ <% end %>
138
+ ```
139
+
140
+ Another good use case for attributes is when you have a component backed by a model:
141
+
142
+ ```ruby
143
+ # app/components/comment_component.rb %>
144
+
145
+ class CommentComponent < SparkComponents::Component
146
+ attribute :comment
147
+
148
+ delegate :id,
149
+ :author,
150
+ :body, to: :comment
151
+ end
152
+ ```
153
+
154
+ ```erb
155
+ <% # app/components/comment/_comment.html.erb %>
156
+
157
+ <div id="comment-<%= comment.id %>" class="comment">
158
+ <div class="comment__author">
159
+ <%= link_to comment.author.name, author_path(comment.author) %>
160
+ </div>
161
+ <div class="comment__body">
162
+ <%= comment.body %>
163
+ </div>
164
+ </div>
165
+ ```
166
+
167
+ ```erb
168
+ <% comments.each do |comment| %>
169
+ <%= component "comment", comment: comment %>
170
+ <% end %>
171
+ ```
172
+
173
+ ### Attribute defaults
174
+
175
+ Attributes can have default values:
176
+
177
+ ```ruby
178
+ # app/components/alert_component.rb %>
179
+
180
+ class AlertComponent < SparkComponents::Component
181
+ attribute :message, context: "primary"
182
+ end
183
+ ```
184
+
185
+ ### Attribute overrides
186
+
187
+ It's easy to override an attribute with additional logic:
188
+
189
+ ```ruby
190
+ # app/components/alert_component.rb %>
191
+
192
+ class AlertComponent < SparkComponents::Component
193
+ attribute :message, context: "primary"
194
+
195
+ def message
196
+ @message.upcase if context == "danger"
197
+ end
198
+ end
199
+ ```
200
+
201
+ ### Attribute validation
202
+
203
+ To ensure your components get initialized properly you can use `ActiveModel::Validations` in your elements or components:
204
+
205
+ ```ruby
206
+ # app/components/alert_component.rb %>
207
+
208
+ class AlertComponent < SparkComponents::Component
209
+ attribute :label
210
+
211
+ validates :label, presence: true
212
+ end
213
+ ```
214
+
215
+ Your validations will be executed during the components initialization and raise an `ActiveModel::ValidationError` if any validation fails.
216
+
217
+ ### Elements
218
+
219
+ Attributes and blocks are great for simple components or components backed by a data structure, such as a model. Other components are more generic in nature and can be used in a variety of contexts. Often they consist of multiple parts or elements, that sometimes repeat, and sometimes need their own modifiers.
220
+
221
+ Take a card component. In React, a common approach is to create subcomponents:
222
+
223
+ ```jsx
224
+ <Card flush={true}>
225
+ <CardHeader centered={true}>
226
+ Header
227
+ </CardHeader>
228
+ <CardSection size="large">
229
+ Section 1
230
+ </CardSection>
231
+ <CardSection size="small">
232
+ Section 2
233
+ </CardSection>
234
+ <CardFooter>
235
+ Footer
236
+ </CardFooter>
237
+ </Card>
238
+ ```
239
+
240
+ There are two problems with this approach:
241
+
242
+ 1. The card header, section and footer have no standalone meaning, yet we treat them as standalone components. This means a `CardHeader` could be placed outside of a `Card`.
243
+ 2. We lose control of the structure of the elements. A `CardHeader` can be placed below, or inside a `CardFooter`.
244
+
245
+ Using this gem, the same component can be written like this:
246
+
247
+ ```ruby
248
+ # app/components/card_component.rb %>
249
+
250
+ class CardComponent < SparkComponents::Component
251
+ attribute flush: false
252
+
253
+ element :header do
254
+ attribute centered: false
255
+ end
256
+
257
+ element :section, multiple: true do
258
+ attribute :size
259
+ end
260
+
261
+ element :footer
262
+ end
263
+ ```
264
+
265
+ ```erb
266
+ <% # app/components/card/_card.html.erb %>
267
+
268
+ <div class="card <%= "card--flush" if card.flush %>">
269
+ <div class="card__header <%= "card__header--centered" if card.header.centered %>">
270
+ <%= card.header %>
271
+ </div>
272
+ <% card.sections.each do |section| %>
273
+ <div class="card__section <%= "card__section--#{section.size}" %>">
274
+ <%= section %>
275
+ </div>
276
+ <% end %>
277
+ <div class="card__footer">
278
+ <%= card.footer %>
279
+ </div>
280
+ </div>
281
+ ```
282
+
283
+ Elements can be thought of as isolated subcomponents, and they are defined on the component. Passing `multiple: true` makes it a repeating element, and passing a block lets us declare attributes on our elements, in the same way we declare attributes on components.
284
+
285
+ In order to populate them with data, we pass a block to the component helper, which yields the component, which lets us set attributes and blocks on the element in the same way we do for components:
286
+
287
+ ```erb
288
+ <%= component "card", flush: true do |c| %>
289
+ <% c.header centered: true do %>
290
+ Header
291
+ <% end %>
292
+ <% c.section size: "large" do %>
293
+ Section 1
294
+ <% end %>
295
+ <% c.section size: "large" do %>
296
+ Section 2
297
+ <% end %>
298
+ <% c.footer do %>
299
+ Footer
300
+ <% end %>
301
+ <% end %>
302
+ ```
303
+
304
+ Multiple calls to a repeating element, such as `section` in the example above, will append each section to an array.
305
+
306
+ Another good use case is a navigation component:
307
+
308
+ ```ruby
309
+ # app/components/navigation_component.rb %>
310
+
311
+ class NavigationComponent < SparkComponents::Component
312
+ element :items, multiple: true do
313
+ attribute :label, :url, active: false
314
+ end
315
+ end
316
+ ```
317
+
318
+ ```erb
319
+ <%= component "navigation" do |c| %>
320
+ <% c.item label: "Home", url: root_path, active: true %>
321
+ <% c.item label: "Explore" url: explore_path %>
322
+ <% end %>
323
+ ```
324
+
325
+ An alternative here is to pass a data structure to the component as an attribute, if no HTML needs to be injected when rendering the component:
326
+
327
+ ```erb
328
+ <%= component "navigation", items: items %>
329
+ ```
330
+
331
+ Elements can have validations, too:
332
+
333
+ ```ruby
334
+ class NavigationComponent < SparkComponents::Component
335
+ element :items, multiple: true do
336
+ attribute :label, :url, active: false
337
+
338
+ validates :label, presence: true
339
+ validates :url, presence: true
340
+ end
341
+ end
342
+ ```
343
+
344
+ Elements can also be nested, although it is recommended to keep nesting to a minimum:
345
+
346
+ ```ruby
347
+ # app/components/card_component.rb %>
348
+
349
+ class CardComponent < SparkComponents::Component
350
+ ...
351
+
352
+ element :section, multiple: true do
353
+ attribute :size
354
+
355
+ element :header
356
+ element :footer
357
+ end
358
+ end
359
+ ```
360
+
361
+ ### Helper methods
362
+
363
+ In addition to declaring attributes and elements, it is also possible to declare helper methods. This is useful if you prefer to keep logic out of your templates. Let's extract the modifier logic from the card component template:
364
+
365
+ ```ruby
366
+ # app/components/card_component.rb %>
367
+
368
+ class CardComponent < SparkComponents::Component
369
+ ...
370
+
371
+ def css_classes
372
+ css_classes = ["card"]
373
+ css_classes << "card--flush" if flush
374
+ css_classes.join(" ")
375
+ end
376
+ end
377
+ ```
378
+
379
+ ```erb
380
+ <% # app/components/card/_card.html.erb %>
381
+
382
+ <%= content_tag :div, class: card.css_classes do %>
383
+ ...
384
+ <% end %>
385
+ ```
386
+
387
+ It's even possible to declare helpers on elements:
388
+
389
+ ```ruby
390
+ # app/components/card_component.rb %>
391
+
392
+ class CardComponent < SparkComponents::Component
393
+ ...
394
+
395
+ element :section, multiple: true do
396
+ attribute :size
397
+
398
+ def css_classes
399
+ css_classes = ["card__section"]
400
+ css_classes << "card__section--#{size}" if size
401
+ css_classes.join(" ")
402
+ end
403
+ end
404
+ end
405
+ ```
406
+
407
+ ```erb
408
+ <% # app/components/card/_card.html.erb %>
409
+
410
+ <%= content_tag :div, class: card.css_classes do %>
411
+ ...
412
+ <%= content_tag :div, class: section.css_classes do %>
413
+ <%= section %>
414
+ <% end %>
415
+ ...
416
+ <% end %>
417
+ ```
418
+
419
+ Helper methods can also make use of the `@view` instance variable in order to call Rails helpers such as `link_to` or `content_tag`.
420
+
421
+ ### Rendering components without a partial
422
+
423
+ For some small components, such as buttons, it might make sense to skip the partial altogether, in order to speed up rendering. This can be done by overriding `render` on the component:
424
+
425
+ ```ruby
426
+ # app/components/button_component.rb %>
427
+
428
+ class ButtonComponent < SparkComponents::Component
429
+ attribute :label, :url, :context
430
+
431
+ def render
432
+ @view.link_to label, url, class: css_classes
433
+ end
434
+
435
+ def css_classes
436
+ css_classes = "button"
437
+ css_classes << "button--#{context}" if context
438
+ css_classes.join(" ")
439
+ end
440
+ end
441
+ ```
442
+
443
+ ```erb
444
+ <%= component "button", label: "Sign up", url: sign_up_path, context: "primary" %>
445
+ <%= component "button", label: "Sign in", url: sign_in_path %>
446
+ ```
447
+
448
+ ### Namespaced components
449
+
450
+ Components can be nested under a namespace. This is useful if you want to practice things like [Atomic Design](http://bradfrost.com/blog/post/atomic-web-design/), [BEMIT](https://csswizardry.com/2015/08/bemit-taking-the-bem-naming-convention-a-step-further/) or any other component classification scheme. In order to create a namespaced component, stick it in a folder and wrap the class in a module:
451
+
452
+ ```ruby
453
+ module Objects
454
+ class MediaObject < SparkComponents::Component; end
455
+ end
456
+ ```
457
+
458
+ Then call it from a template like so:
459
+
460
+ ```erb
461
+ <%= component "objects/media_object" %>
462
+ ```
463
+
464
+ ### Nesting Components
465
+
466
+ You can easily render a component within another component. In this example the nav_item component is a generic navigation element which is used in both the tab component and the breadcrumb component.
467
+
468
+ ```ruby
469
+ # app/components/nav_item_component.rb %>
470
+
471
+ class NavItemComponent < SparkComponents::Components
472
+ attribute :name, :url, :icon
473
+ end
474
+ ```
475
+
476
+ ```ruby
477
+ # app/components/tabs_component.rb %>
478
+
479
+ class TabsComponent < SparkComponents::Components
480
+ element :tab, multiple: true, component: 'nav_item'
481
+ end
482
+ ```
483
+
484
+ ```ruby
485
+ # app/components/breadcrumb_component.rb %>
486
+
487
+ class BreadcrumbComponent < SparkComponents::Components
488
+ element :crumb, multiple: true, component: 'nav_item'
489
+ end
490
+ ```
491
+
492
+ The erb template could look like this.
493
+
494
+ ```erb
495
+ <% # app/components/tab_nav/_tab_nav.html.erb %>
496
+
497
+ <nav class="tab__nav" role="navigation">
498
+ <% tabs.each do |tab| %>
499
+ <%= tab %>
500
+ <% end %>
501
+ </nav>
502
+ ```
503
+
504
+ Then a user would use the nav_item component as an element of the tab_nav component like this.
505
+
506
+ ```erb
507
+ <%= component "tab_nav" do |nav| %>
508
+ <% nav.tab name: "Home", url: "/home", icon: 'house' %>
509
+ <% nav.tab name: "Search", url: "/search", icon: 'magnifing_glass' %>
510
+ ...
511
+ <% end %>
512
+ ```
513
+
514
+ ## Acknowledgements
515
+
516
+ This library, together with [styleguide](https://github.com/jensljungblad/styleguide), was inspired by the writings of [Brad Frost](http://bradfrost.com) on atomic design and living style guides, and [Rizzo](http://rizzo.lonelyplanet.com), the Lonely Planet style guide. Other inspirations were:
517
+
518
+ - [Catalog](https://www.catalog.style) - style guide for React
519
+ - [Storybook](https://storybook.js.org) - style guide for React
520
+ - [React Styleguidist](https://react-styleguidist.js.org) - style guide for React
521
+ - [Cells](https://github.com/trailblazer/cells) - view components for Ruby
522
+ - [Komponent](https://github.com/komposable/komponent) - view components for Ruby
523
+
524
+ For a list of real world style guides, check out http://styleguides.io.
data/Rakefile ADDED
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "bundler/setup"
5
+ rescue LoadError
6
+ puts "You must `gem install bundler` and `bundle install` to run rake tasks"
7
+ end
8
+
9
+ require "rdoc/task"
10
+
11
+ RDoc::Task.new(:rdoc) do |rdoc|
12
+ rdoc.rdoc_dir = "rdoc"
13
+ rdoc.title = "Components"
14
+ rdoc.options << "--line-numbers"
15
+ rdoc.rdoc_files.include("README.md")
16
+ rdoc.rdoc_files.include("lib/**/*.rb")
17
+ end
18
+
19
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
20
+ load "rails/tasks/engine.rake"
21
+
22
+ load "rails/tasks/statistics.rake"
23
+
24
+ require "bundler/gem_tasks"
25
+
26
+ require "rake/testtask"
27
+
28
+ Rake::TestTask.new(:test) do |t|
29
+ t.libs << "test"
30
+ t.pattern = "test/**/*_test.rb"
31
+ t.verbose = false
32
+ end
33
+
34
+ task default: :test
@@ -0,0 +1,2 @@
1
+ //= link_directory ../javascripts .js
2
+ //= link_directory ../stylesheets .css
@@ -0,0 +1,7 @@
1
+ <% SparkComponents.component_names.each do |name| %>
2
+ <% begin %>
3
+ <% require_asset "#{name}/#{name.split('/')[-1]}" %>
4
+ <% rescue Sprockets::FileNotFound %>
5
+ <% Rails.logger.debug "Components: JS not found for #{name}" %>
6
+ <% end %>
7
+ <% end %>
@@ -0,0 +1,7 @@
1
+ <% SparkComponents.component_names.each do |name| %>
2
+ <% begin %>
3
+ <% require_asset "#{name}/#{name.split('/')[-1]}" %>
4
+ <% rescue Sprockets::FileNotFound %>
5
+ <% Rails.logger.debug "Components: CSS not found for #{name}" %>
6
+ <% end %>
7
+ <% end %>
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SparkComponents
4
+ module ComponentHelper
5
+ def component(name, attrs = nil, &block)
6
+ comp = "#{name}_component".classify.constantize.new(self, attrs, &block)
7
+ comp.pre_render
8
+ comp.render
9
+ end
10
+ end
11
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ SparkComponents::Engine.routes.draw do
4
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SparkComponents
4
+ class ComponentGenerator < Rails::Generators::NamedBase
5
+ desc "Generate a component"
6
+ class_option :skip_erb, type: :boolean, default: false
7
+ class_option :skip_css, type: :boolean, default: false
8
+ class_option :skip_js, type: :boolean, default: false
9
+
10
+ source_root File.expand_path("templates", __dir__)
11
+
12
+ def create_component_file
13
+ template "component.rb.erb", "app/components/#{name}_component.rb"
14
+ end
15
+
16
+ def create_erb_file
17
+ return if options["skip_erb"]
18
+
19
+ create_file "app/components/#{name}/_#{filename}.html.erb"
20
+ end
21
+
22
+ def create_css_file
23
+ return if options["skip_css"]
24
+
25
+ create_file "app/components/#{name}/#{filename}.css"
26
+ end
27
+
28
+ def create_js_file
29
+ return if options["skip_js"]
30
+
31
+ create_file "app/components/#{name}/#{filename}.js"
32
+ end
33
+
34
+ private
35
+
36
+ def filename
37
+ name.split("/").last
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,2 @@
1
+ class <%= name.classify %>Component < SparkComponents::Component
2
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SparkComponents
4
+ module Attributes
5
+ class Hash < Hash
6
+ def prefix; end
7
+
8
+ def add(obj = nil)
9
+ merge!(obj) unless obj.nil?
10
+ self
11
+ end
12
+
13
+ # Output all attributes as [base-]name="value"
14
+ def to_s
15
+ each_with_object([]) do |(name, value), array|
16
+ name = [prefix, name].compact.join("-")
17
+ array << %(#{name.dasherize}="#{value}") unless value.nil?
18
+ end.join(" ").html_safe
19
+ end
20
+
21
+ def collapse
22
+ each_with_object({}) do |(name, value), obj|
23
+ name = [prefix, name].compact.join("-")
24
+ name = name.downcase.gsub(/[\W_]+/, "-")
25
+ obj[name] = value unless value.nil? || value.is_a?(String) && value.empty?
26
+ end
27
+ end
28
+ end
29
+
30
+ class Data < Hash
31
+ def prefix
32
+ :data
33
+ end
34
+ end
35
+
36
+ class Aria < Hash
37
+ def prefix
38
+ :aria
39
+ end
40
+ end
41
+
42
+ class Classname < Array
43
+ def initialize(*args, &block)
44
+ super(*args, &block)
45
+ @base_set = false
46
+ end
47
+
48
+ # Many elements have a base class which defines core utlitiy
49
+ # This classname may serve as a root for other element classnames
50
+ # and should be distincly accessible
51
+ #
52
+ # For example:
53
+ # classes = Classname.new
54
+ # classes.base = 'nav__item'
55
+ # now generate a wrapper: "#{classes.base}__wrapper"
56
+ #
57
+ # Ensure base class is the first element in the classes array.
58
+ #
59
+ def base=(klass)
60
+ return if klass.blank?
61
+
62
+ if @base_set
63
+ self[0] = klass
64
+ else
65
+ unshift klass
66
+ @base_set = true
67
+ end
68
+
69
+ uniq!
70
+ end
71
+
72
+ def base
73
+ first if @base_set
74
+ end
75
+
76
+ # Returns clasess which are not defined as a base class
77
+ def modifiers
78
+ @base_set ? self[1..size] : self
79
+ end
80
+
81
+ def add(*args)
82
+ push(*args.uniq.reject { |a| a.nil? || include?(a) })
83
+ end
84
+
85
+ def to_s
86
+ join(" ").html_safe
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SparkComponents
4
+ class Component < Element
5
+ def self.model_name
6
+ ActiveModel::Name.new(SparkComponents::Component)
7
+ end
8
+
9
+ def self.component_name
10
+ class_name.chomp("Component").demodulize.underscore
11
+ end
12
+
13
+ def self.component_path
14
+ class_name.chomp("Component").underscore
15
+ end
16
+
17
+ # Allow Components to lookup their original classname
18
+ # even if created with Class.new(SomeComponent)
19
+ def self.class_name
20
+ name || superclass.name
21
+ end
22
+
23
+ def render
24
+ render_partial to_partial_path
25
+ end
26
+
27
+ def _name
28
+ super || self.class.component_name
29
+ end
30
+
31
+ def to_partial_path
32
+ [self.class.component_path, self.class.component_name].join("/")
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,276 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SparkComponents
4
+ class Element
5
+ include ActiveModel::Validations
6
+
7
+ attr_accessor :yield
8
+ attr_reader :parents, :attr
9
+
10
+ def self.model_name
11
+ ActiveModel::Name.new(SparkComponents::Element)
12
+ end
13
+
14
+ def self.attributes
15
+ @attributes ||= {}
16
+ end
17
+
18
+ def self.elements
19
+ @elements ||= {}
20
+ end
21
+
22
+ def self.attribute(*args)
23
+ args.each_with_object({}) do |arg, obj|
24
+ if arg.is_a?(Hash)
25
+ arg.each do |attr, default|
26
+ obj[attr.to_sym] = default
27
+ set_attribute(attr.to_sym, default: default)
28
+ end
29
+ else
30
+ obj[arg.to_sym] = nil
31
+ set_attribute(arg.to_sym)
32
+ end
33
+ end
34
+ end
35
+
36
+ def self.set_attribute(name, default: nil)
37
+ attributes[name] = { default: default }
38
+
39
+ define_method_or_raise(name) do
40
+ get_instance_variable(name)
41
+ end
42
+ end
43
+
44
+ def self.base_class(name)
45
+ tag_attributes[:class].base = name
46
+ end
47
+
48
+ def self.add_class(*args)
49
+ tag_attributes[:class].add(*args)
50
+ end
51
+
52
+ def self.data_attr(*args)
53
+ set_attr(:data, *args)
54
+ end
55
+
56
+ def self.aria_attr(*args)
57
+ set_attr(:aria, *args)
58
+ end
59
+
60
+ def self.tag_attr(*args)
61
+ set_attr(:tag, *args)
62
+ end
63
+
64
+ def self.set_attr(name, *args)
65
+ tag_attributes[name].add(attribute(*args))
66
+ end
67
+
68
+ def self.tag_attributes
69
+ @tag_attributes ||= {
70
+ class: SparkComponents::Attributes::Classname.new,
71
+ data: SparkComponents::Attributes::Data.new,
72
+ aria: SparkComponents::Attributes::Aria.new,
73
+ tag: SparkComponents::Attributes::Hash.new
74
+ }
75
+ end
76
+
77
+ # rubocop:disable Metrics/AbcSize
78
+ # rubocop:disable Metrics/CyclomaticComplexity
79
+ # rubocop:disable Metrics/MethodLength
80
+ # rubocop:disable Metrics/PerceivedComplexity
81
+ def self.element(name, multiple: false, component: nil, &config)
82
+ plural_name = name.to_s.pluralize.to_sym if multiple
83
+
84
+ # Extend components by string or class; e.g., "core/header" or Core::HeaderComponent
85
+ component = "#{component}_component".classify.constantize if component.is_a?(String)
86
+
87
+ elements[name] = {
88
+ multiple: plural_name || false, class: Class.new(component || Element, &config)
89
+ }
90
+
91
+ define_method_or_raise(name) do |attributes = nil, &block|
92
+ return get_instance_variable(multiple ? plural_name : name) unless attributes || block
93
+
94
+ element = self.class.elements[name][:class].new(@view, attributes, &block)
95
+ element.parent = self
96
+
97
+ if element.respond_to?(:render)
98
+ element.pre_render
99
+ element.yield = element.render
100
+ end
101
+
102
+ if multiple
103
+ get_instance_variable(plural_name) << element
104
+ else
105
+ set_instance_variable(name, element)
106
+ end
107
+ end
108
+
109
+ return if !multiple || name == plural_name
110
+
111
+ define_method_or_raise(plural_name) do
112
+ get_instance_variable(plural_name)
113
+ end
114
+ end
115
+ # rubocop:enable Metrics/AbcSize
116
+ # rubocop:enable Metrics/CyclomaticComplexity
117
+ # rubocop:enable Metrics/MethodLength
118
+ # rubocop:enable Metrics/PerceivedComplexity
119
+
120
+ def self.define_method_or_raise(method_name, &block)
121
+ # Select instance methods but not those which are intance methods received by extending a class
122
+ methods = (instance_methods - superclass.instance_methods(false))
123
+ raise(SparkComponents::Error, "Method '#{method_name}' already exists.") if methods.include?(method_name.to_sym)
124
+
125
+ define_method(method_name, &block)
126
+ end
127
+ private_class_method :define_method_or_raise
128
+
129
+ def self.inherited(subclass)
130
+ attributes.each { |name, options| subclass.set_attribute(name, options.dup) }
131
+ elements.each { |name, options| subclass.elements[name] = options.dup }
132
+
133
+ subclass.tag_attributes.merge!(tag_attributes.each_with_object({}) do |(k, v), obj|
134
+ obj[k] = v.dup
135
+ end)
136
+ end
137
+
138
+ def initialize(view, attributes = nil, &block)
139
+ @view = view
140
+ attributes ||= {}
141
+ initialize_tag_attributes
142
+ assign_tag_attributes(attributes)
143
+ initialize_attributes(attributes)
144
+ initialize_elements
145
+ @yield = block_given? ? @view.capture(self, &block) : nil
146
+ validate!
147
+ after_init
148
+ end
149
+
150
+ def pre_render; end
151
+
152
+ def after_init; end
153
+
154
+ def parent=(obj)
155
+ @parents = [obj.parents, obj].flatten.compact
156
+ end
157
+
158
+ def parent
159
+ @parents.last
160
+ end
161
+
162
+ # Set tag attribute values from from parameters
163
+ def update_attr(name)
164
+ %i[aria data tag].each do |el|
165
+ @tag_attributes[el][name] = get_instance_variable(name) if @tag_attributes[el].key?(name)
166
+ end
167
+ end
168
+
169
+ def classnames
170
+ @tag_attributes[:class]
171
+ end
172
+
173
+ def base_class(name = nil)
174
+ classnames.base = name unless name.nil?
175
+ classnames.base
176
+ end
177
+
178
+ def add_class(*args)
179
+ classnames.add(*args)
180
+ end
181
+
182
+ def join_class(name, separator: "-")
183
+ [base_class, name].join(separator) unless base_class.nil?
184
+ end
185
+
186
+ def data_attr(*args)
187
+ @tag_attributes[:data].add(*args)
188
+ end
189
+
190
+ def aria_attr(*args)
191
+ @tag_attributes[:aria].add(*args)
192
+ end
193
+
194
+ def tag_attr(*args)
195
+ @tag_attributes[:tag].add(*args)
196
+ end
197
+
198
+ def attrs(add_class: true)
199
+ atr = Attributes::Hash.new
200
+ # attrtiubte order: id, class, data-, aria-, misc tag attributes
201
+ atr[:id] = tag_attr.delete(:id)
202
+ atr[:class] = classnames if add_class
203
+ atr.merge!(data_attr.collapse)
204
+ atr.merge!(aria_attr.collapse)
205
+ atr.merge!(tag_attr)
206
+ atr
207
+ end
208
+
209
+ def concat(*args, &block)
210
+ @view.concat(*args, &block)
211
+ end
212
+
213
+ def content_tag(*args, &block)
214
+ @view.content_tag(*args, &block)
215
+ end
216
+
217
+ def link_to(*args, &block)
218
+ @view.link_to(*args, &block)
219
+ end
220
+
221
+ def component(*args, &block)
222
+ @view.component(*args, &block)
223
+ end
224
+
225
+ def to_s
226
+ @yield
227
+ end
228
+
229
+ protected
230
+
231
+ def render_partial(file)
232
+ @view.render(partial: file, object: self)
233
+ end
234
+
235
+ def initialize_tag_attributes
236
+ @tag_attributes = self.class.tag_attributes.each_with_object({}) do |(name, options), obj|
237
+ obj[name] = options.dup
238
+ end
239
+ end
240
+
241
+ def assign_tag_attributes(attributes)
242
+ # support default data, class, and aria attribute names
243
+ data_attr(attributes.delete(:data)) if attributes[:data]
244
+ aria_attr(attributes.delete(:aria)) if attributes[:aria]
245
+ add_class(*attributes.delete(:class)) if attributes[:class]
246
+ tag_attr(attributes.delete(:splat)) if attributes[:splat]
247
+ end
248
+
249
+ def initialize_attributes(attributes)
250
+ self.class.attributes.each do |name, options|
251
+ set_instance_variable(name, attributes[name] || (options[:default] && options[:default].dup))
252
+ update_attr(name)
253
+ end
254
+ end
255
+
256
+ def initialize_elements
257
+ self.class.elements.each do |name, options|
258
+ if (plural_name = options[:multiple])
259
+ set_instance_variable(plural_name, [])
260
+ else
261
+ set_instance_variable(name, nil)
262
+ end
263
+ end
264
+ end
265
+
266
+ private
267
+
268
+ def get_instance_variable(name)
269
+ instance_variable_get(:"@#{name}")
270
+ end
271
+
272
+ def set_instance_variable(name, value)
273
+ instance_variable_set(:"@#{name}", value)
274
+ end
275
+ end
276
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SparkComponents
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace SparkComponents
6
+
7
+ initializer "components.asset_paths" do |app|
8
+ app.config.assets.paths << SparkComponents.components_path if app.config.respond_to?(:assets)
9
+ end
10
+
11
+ initializer "components.view_helpers" do
12
+ ActiveSupport.on_load :action_controller do
13
+ helper SparkComponents::ComponentHelper
14
+ end
15
+
16
+ ActiveSupport.on_load :action_view do
17
+ include SparkComponents::ComponentHelper
18
+ end
19
+ end
20
+
21
+ initializer "components.view_paths" do
22
+ ActiveSupport.on_load :action_controller do
23
+ append_view_path SparkComponents.components_path
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SparkComponents
4
+ class Railtie < ::Rails::Railtie
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SparkComponents
4
+ VERSION = "1.0.0"
5
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spark_components/attributes"
4
+ require "spark_components/element"
5
+ require "spark_components/component"
6
+ require "spark_components/engine"
7
+
8
+ module SparkComponents
9
+ class Error < StandardError; end
10
+
11
+ def self.components_path
12
+ Rails.root.join("app", "components")
13
+ end
14
+
15
+ def self.component_names
16
+ return [] unless Dir.exist?(components_path)
17
+
18
+ Dir.chdir(components_path) do
19
+ Dir.glob("**/*_component.rb").map { |component| component.chomp("_component.rb") }.sort
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # desc "Explaining what the task does"
4
+ # task :components do
5
+ # # Task goes here
6
+ # end
metadata ADDED
@@ -0,0 +1,103 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: spark_components
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Brandon Mathis
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-05-10 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: 5.1.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 5.1.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: rubocop
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: sqlite3
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: Simple view components for Rails 5.1+
56
+ email:
57
+ - brandon@imathis.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - MIT-LICENSE
63
+ - README.md
64
+ - Rakefile
65
+ - app/assets/config/components_manifest.js
66
+ - app/assets/javascripts/components.js.erb
67
+ - app/assets/stylesheets/components.css.erb
68
+ - app/helpers/spark_components/component_helper.rb
69
+ - config/routes.rb
70
+ - lib/generators/spark_components/component_generator.rb
71
+ - lib/generators/spark_components/templates/component.rb.erb
72
+ - lib/spark_components.rb
73
+ - lib/spark_components/attributes.rb
74
+ - lib/spark_components/component.rb
75
+ - lib/spark_components/element.rb
76
+ - lib/spark_components/engine.rb
77
+ - lib/spark_components/railtie.rb
78
+ - lib/spark_components/version.rb
79
+ - lib/tasks/components_tasks.rake
80
+ homepage: https://github.com/imathis/spark_components
81
+ licenses:
82
+ - MIT
83
+ metadata: {}
84
+ post_install_message:
85
+ rdoc_options: []
86
+ require_paths:
87
+ - lib
88
+ required_ruby_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ required_rubygems_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ requirements: []
99
+ rubygems_version: 3.0.3
100
+ signing_key:
101
+ specification_version: 4
102
+ summary: Simple view components for Rails 5.1+
103
+ test_files: []