lite-component 1.0.1 → 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![Build Status](https://travis-ci.org/drexed/lite-component.svg?branch=master)](https://travis-ci.org/drexed/lite-component)
5
5
 
6
6
  Lite::Component is a library for building component base objects. This technique simplifies
7
- and organizes often used or logically comply page objects.
7
+ and organizes often used or logically complex page objects.
8
8
 
9
9
  ## Installation
10
10
 
@@ -27,443 +27,241 @@ Or install it yourself as:
27
27
  * [Setup](#setup)
28
28
  * [Generator](#generator)
29
29
  * [Assets](#assets)
30
- * [Components](#components)
30
+ * [Routes](#routes)
31
31
  * [Usage](#usage)
32
- * [Attribute and blocks](#attributes-and-blocks)
33
- * [Attributes defaults](#attribute-defaults)
34
- * [Attributes overrides](#attribute-overrides)
35
- * [Elements](#elements)
36
- * [Helper methods](#helper-methods)
37
- * [Rendering components without a partial](#rendering-components-without-a-partial)
38
- * [Namespaced components](#namespaced-components)
32
+ * [Rendering](#rendering)
33
+ * [Context](#context)
34
+ * [Helpers](#helpers)
35
+ * [Locals](#locals)
36
+ * [Iterations](#iterations)
37
+ * [Views](#views)
39
38
 
40
39
  ## Setup
41
40
 
42
41
  ### Generator
43
42
 
44
43
  Use `rails g component NAME` will generate the following files:
44
+
45
45
  ```
46
- /app/assets/javascripts/components/[name].js
47
- /app/assets/stylesheets/components/[name].scss
48
- /app/components/[name]_query.rb
49
- /app/views/components/_[name].html.erb
46
+ app/assets/javascripts/components/[name].js
47
+ app/assets/stylesheets/components/[name].scss
48
+ app/components/[name]_query.rb
49
+ app/views/components/_[name].html.erb
50
50
  ```
51
- The generator also takes `--skip-css`, `--skip-js` and `--skip-erb` options
52
-
53
- ### Assets
54
-
55
- In the basic Rails app setup component `*.scss` and `*.js` will be automatically load
56
- via the tree lookup.
57
51
 
58
- In order to require assets such manually require them in the manifest, e.g. `application.css`:
59
- *Similar process for both CSS and JS*
52
+ The generator also takes `--skip-css`, `--skip-js` and `--skip-erb` options. It will also
53
+ properly namespace nested components.
60
54
 
61
- ```
62
- /*
63
- * All components:
64
- *= require lite-component
65
- *
66
- * - or -
67
- *
68
- * Specific component:
69
- *= require component/alert
70
- */
71
- ```
55
+ If a `ApplicationComponent` file in the `app/components` directory is available, the
56
+ generator will create file that inherit from `ApplicationComponent` if not it will
57
+ fallback to `Lite::Component::Base`.
72
58
 
73
- ### Components
59
+ ### Assets
74
60
 
75
- If you create a `ApplicationComponent` file in the `app/components` directory, the generator
76
- will create file that inherit from `ApplicationComponent` if not `Lite::Component::Base`.
61
+ Component's `*.scss` and `*.js` will be automatically load via the tree lookup in basic
62
+ Rails setups.
77
63
 
78
- Components come with view helpers already included.
64
+ ### Routes
79
65
 
80
- If you want to access route helpers in your components just include them like:
66
+ If you want to access route helpers in `*_component.rb` just include them like:
81
67
 
82
68
  ```ruby
83
69
  # app/components/alert_component.rb
84
70
 
85
- class AlertComponent < Components::Component
71
+ class AlertComponent < Lite::Component::Base
86
72
  include Rails.application.routes.url_helpers
87
- end
88
- ```
89
-
90
- ## Usage
91
-
92
- ### Attributes and blocks
93
73
 
94
- 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
95
- to inject HTML content into components.
96
-
97
- ```ruby
98
- # app/components/alert_component.rb
74
+ def link_to_account
75
+ link_to('Return to account', account_path, class: 'text-underline')
76
+ end
99
77
 
100
- class AlertComponent < Components::Component
101
- attribute :context
102
- attribute :message
103
78
  end
104
79
  ```
105
80
 
106
- ```erb
107
- <% # app/views/components/_alert.html.erb %>
108
-
109
- <div class="alert alert--<%= alert.context %>" role="alert">
110
- <%= alert.message %>
111
- </div>
112
- ```
113
-
114
- ```erb
115
- <%= component "alert", message: "Something went right!", context: "success" %>
116
- <%= component AlertComponent, message: "Something went wrong!", context: "danger" %>
117
- ```
118
-
119
- 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:
81
+ ## Usage
120
82
 
121
- ```erb
122
- <% # app/views/components/_alert.html.erb %>
83
+ ### Rendering
123
84
 
124
- <div class="alert alert--<%= alert.context %>" role="alert">
125
- <%= alert %>
126
- </div>
127
- ```
85
+ To render a component in any view template or partial, you can use the the provided helper.
86
+ Its has the same setup as `render` and takes all [Action View Partials](https://api.rubyonrails.org/classes/ActionView/PartialRenderer.html)
87
+ options.
128
88
 
129
89
  ```erb
130
- <%= component "alert", context: "success" do %>
131
- <em>Something</em> went right!
132
- <% end %>
90
+ <%= component("alert") %>
91
+ <%= component(AlertComponent, locals: { message: "Something went right!", type: "success" }) %>
133
92
  ```
134
93
 
135
- Another good use case for attributes is when you have a component backed by a model:
136
-
137
- ```ruby
138
- # app/components/comment_component.rb
139
-
140
- class CommentComponent < Components::Component
141
- attribute :comment
142
-
143
- delegate :id, :author, :body, to: :comment
144
- end
145
- ```
146
-
147
- ```erb
148
- <% # app/views/components/_comment.html.erb %>
149
-
150
- <div id="comment-<%= comment.id %>" class="comment">
151
- <div class="comment__author">
152
- <%= link_to comment.author.name, author_path(comment.author) %>
153
- </div>
154
- <div class="comment__body">
155
- <%= comment.body %>
156
- </div>
157
- </div>
158
- ```
94
+ Render namespaced components by following standard naming conventions:
159
95
 
160
96
  ```erb
161
- <% comments.each do |comment| %>
162
- <%= component "comment", comment: comment %>
163
- <% end %>
97
+ <%= component("admin/alert") %>
98
+ <%= component(Admin::AlertComponent) %>
164
99
  ```
165
100
 
166
- ### Attribute defaults
101
+ Render collection of components just as you would render collections of partials.
167
102
 
168
- Attributes can have default values:
169
-
170
- ```ruby
171
- # app/components/alert_component.rb
172
-
173
- class AlertComponent < Components::Component
174
- attribute :message
175
- attribute :context, default: "primary"
176
- end
103
+ ```erb
104
+ <%= component("comment_card", collection: @comments, spacer_template: "components/spacer") %>
177
105
  ```
178
106
 
179
- ### Attribute overrides
107
+ ### Context
180
108
 
181
- It's easy to override an attribute with additional logic:
109
+ All components include `ActionView::Context` which will give you access to request context such as
110
+ helpers, controllers, etc. It can be accessed using `context` or `c` methods.
182
111
 
183
112
  ```ruby
184
113
  # app/components/alert_component.rb
185
114
 
186
- class AlertComponent < Components::Component
187
- attribute :message
188
- attribute :context, default: "primary"
115
+ class AlertComponent < Lite::Component::Base
189
116
 
190
- def message
191
- @message.upcase if context == "danger"
117
+ def protected_page?
118
+ context.controller_name == 'admin'
192
119
  end
193
- end
194
- ```
195
-
196
- ### Attribute validation
197
-
198
- To ensure your components get initialized properly you can use `ActiveModel::Validations` in your elements or components:
199
-
200
- ```ruby
201
- # app/components/alert_component.rb
202
-
203
- class AlertComponent < Components::Component
204
- attribute :label
205
120
 
206
- validates :label, presence: true
207
121
  end
208
122
  ```
209
123
 
210
- Your validations will be executed during the components initialization and raise an `Lite::Component::ValidationError` if any validation fails.
211
-
212
- ### Elements
213
-
214
- 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.
215
-
216
- Take a card component. In React, a common approach is to create subcomponents:
217
-
218
- ```jsx
219
- <Card flush={true}>
220
- <CardHeader centered={true}>
221
- Header
222
- </CardHeader>
223
- <CardSection size="large">
224
- Section 1
225
- </CardSection>
226
- <CardSection size="small">
227
- Section 2
228
- </CardSection>
229
- <CardFooter>
230
- Footer
231
- </CardFooter>
232
- </Card>
233
- ```
234
-
235
- There are two problems with this approach:
124
+ ### Helpers
236
125
 
237
- 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`.
238
- 2. We lose control of the structure of the elements. A `CardHeader` can be placed below, or inside a `CardFooter`.
239
-
240
- Using this gem, the same component can be written like this:
126
+ All components include `ActionView::Helpers` which will give you access to default Rails
127
+ helpers without the need to invoke the context. Use the helper methods to access helper methods
128
+ from your `app/helpers` directory. It can be accessed using `helpers` or `h` methods.
241
129
 
242
130
  ```ruby
243
- # app/components/card_component.rb
131
+ # app/components/alert_component.rb
244
132
 
245
- class CardComponent < Components::Component
246
- attribute :flush, default: false
133
+ class AlertComponent < Lite::Component::Base
247
134
 
248
- element :header do
249
- attribute :centered, default: false
135
+ def close_icon
136
+ h.icon_tag(:close)
250
137
  end
251
138
 
252
- element :section, multiple: true do
253
- attribute :size
139
+ def link_to_close
140
+ link_to(close_icon, '#', data: { alert: :dismiss })
254
141
  end
255
142
 
256
- element :footer
257
143
  end
258
144
  ```
259
145
 
260
- ```erb
261
- <% # app/views/components/_card.html.erb %>
262
-
263
- <div class="card <%= "card--flush" if card.flush %>">
264
- <div class="card__header <%= "card__header--centered" if card.header.centered %>">
265
- <%= card.header %>
266
- </div>
267
- <% card.sections.each do |section| %>
268
- <div class="card__section <%= "card__section--#{section.size}" %>">
269
- <%= section %>
270
- </div>
271
- <% end %>
272
- <div class="card__footer">
273
- <%= card.footer %>
274
- </div>
275
- </div>
276
- ```
277
-
278
- 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.
146
+ ### Locals
279
147
 
280
- 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:
148
+ All components include access to partial locals via the `locals` or `l` methods.
149
+ *Note: Objects will be automatically added to locals when rendering collections.*
281
150
 
282
151
  ```erb
283
- <%= component "card", flush: true do |c| %>
284
- <% c.header centered: true do %>
285
- Header
286
- <% end %>
287
- <% c.section size: "large" do %>
288
- Section 1
289
- <% end %>
290
- <% c.section size: "large" do %>
291
- Section 2
292
- <% end %>
293
- <% c.footer do %>
294
- Footer
295
- <% end %>
296
- <% end %>
152
+ <%= component("alert", locals: { object: @user }) %>
297
153
  ```
298
154
 
299
- Multiple calls to a repeating element, such as `section` in the example above, will append each section to an array.
300
-
301
- Another good use case is a navigation component:
302
-
303
155
  ```ruby
304
- # app/components/navigation_component.rb
156
+ # app/components/alert_component.rb
157
+
158
+ class AlertComponent < Lite::Component::Base
305
159
 
306
- class NavigationComponent < Components::Component
307
- element :items, multiple: true do
308
- attribute :label
309
- attribute :url
310
- attribute :active, default: false
160
+ def type_tag
161
+ <<~HTML.squish.html_safe
162
+ <b>#{locals.object.first_name}!</b>
163
+ HTML
311
164
  end
165
+
312
166
  end
313
167
  ```
314
168
 
315
- ```erb
316
- <%= component "navigation" do |c| %>
317
- <% c.item label: "Home", url: root_path, active: true %>
318
- <% c.item label: "Explore" url: explore_path %>
319
- <% end %>
320
- ```
169
+ ### Iterations
321
170
 
322
- 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:
171
+ All components will hav access to an iteration object which can be accessed
172
+ using the `iteration` or `i` methods. It provides access to each iterations
173
+ `first?`, `last?`, `size`, and `index` methods.
323
174
 
324
175
  ```erb
325
- <%= component "navigation", items: items %>
176
+ <%= component("alert", collection: @users) %>
326
177
  ```
327
178
 
328
- Elements can have validations, too:
329
-
330
179
  ```ruby
331
- # app/components/navigation_component.rb
180
+ # app/components/alert_component.rb
332
181
 
333
- class NavigationComponent < Components::Component
334
- element :items, multiple: true do
335
- attribute :label
336
- attribute :url
337
- attribute :active, default: false
182
+ class AlertComponent < Lite::Component::Base
338
183
 
339
- validates :label, presence: true
340
- validates :url, presence: true
184
+ def limit_hit?
185
+ i.index == 5
341
186
  end
342
- end
343
- ```
344
-
345
- Elements can also be nested, although it is recommended to keep nesting to a minimum:
346
-
347
- ```ruby
348
- # app/components/card_component.rb
349
187
 
350
- class CardComponent < Components::Component
351
- ...
352
-
353
- element :section, multiple: true do
354
- attribute :size
355
-
356
- element :header
357
- element :footer
358
- end
359
188
  end
360
189
  ```
361
190
 
362
- ### Helper methods
191
+ ### Views
363
192
 
364
- 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:
193
+ *For the following examples, components will have the following setup:*
365
194
 
366
195
  ```ruby
367
- # app/components/card_component.rb
196
+ # app/components/alert_component.rb
368
197
 
369
- class CardComponent < Components::Component
370
- ...
198
+ class AlertComponent < Lite::Component::Base
371
199
 
372
- def css_classes
373
- css_classes = ["card"]
374
- css_classes << "card--flush" if flush
375
- css_classes.join(" ")
200
+ def link_to_back
201
+ link_to('Go back', :back, class: 'text-underline')
376
202
  end
203
+
377
204
  end
378
205
  ```
379
206
 
380
207
  ```erb
381
- <% # app/views/components/_card.html.erb %>
382
-
383
- <%= content_tag :div, class: card.css_classes do %>
384
- ...
385
- <% end %>
208
+ <%= component("alert", collection: @users, locals: { message: "Something went right!", type: "success" }) %>
386
209
  ```
387
210
 
388
- It's even possible to declare helpers on elements:
211
+ Component view partials behave just as a normal view partial would. All locals can be
212
+ accessed by their key.
389
213
 
390
- ```ruby
391
- # app/components/card_component.rb %>
214
+ ```erb
215
+ <%# app/views/components/_alert.html.erb %>
392
216
 
393
- class CardComponent < Components::Component
394
- ...
217
+ <div class="alert alert-<%= type %>" role="alert">
218
+ <%= message %>
219
+ </div>
220
+ ```
395
221
 
396
- element :section, multiple: true do
397
- attribute :size
222
+ Access to anything provided within its `*_component.rb` file can be done using the
223
+ `component` local which is the instance of the component.
398
224
 
399
- def css_classes
400
- css_classes = ["card__section"]
401
- css_classes << "card__section--#{size}" if size
402
- css_classes.join(" ")
403
- end
404
- end
405
- end
225
+ ```erb
226
+ <%# app/views/components/_alert.html.erb %>
227
+
228
+ <div class="alert alert-<%= type %>" role="alert">
229
+ <%= component.locals.message %>
230
+ <%= component.link_to_back %>
231
+ </div>
406
232
  ```
407
233
 
234
+ Rendering a collection will automatically give you access to the iteration and object variables.
235
+
408
236
  ```erb
409
- <% # app/views/components/_card.html.erb %>
237
+ <%# app/views/components/_alert.html.erb %>
410
238
 
411
- <%= content_tag :div, class: card.css_classes do %>
412
- ...
413
- <%= content_tag :div, class: section.css_classes do %>
414
- <%= section %>
239
+ <div class="alert alert-<%= type %>" role="alert">
240
+ <% if iteration.size > 1 %>
241
+ Alert #<%= iteration.index %>
242
+ <% content_tag(:br, nil) unless iteration.last? %>
415
243
  <% end %>
416
- ...
417
- <% end %>
418
- ```
419
-
420
- Helper methods can also make use of the `@view` instance variable in order to call Rails helpers such as `link_to` or `content_tag`.
421
244
 
422
- ### Rendering components without a partial
245
+ Hi <%= object.first_name %>,
246
+ <br />
247
+ <%= message %>
248
+ </div>
249
+ ```
423
250
 
424
- 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:
251
+ To bypass having a partial and just rendering content directly override the `render_content` method.
425
252
 
426
253
  ```ruby
427
- # app/components/button_component.rb
428
-
429
- class ButtonComponent < Components::Component
430
- attribute :label
431
- attribute :url
432
- attribute :context
254
+ # app/components/alert_component.rb
433
255
 
434
- def render
435
- @view.link_to label, url, class: css_classes
436
- end
256
+ class AlertComponent < Lite::Component::Base
437
257
 
438
- def css_classes
439
- css_classes = "button"
440
- css_classes << "button--#{context}" if context
441
- css_classes.join(" ")
258
+ def render_content
259
+ content_tag(:span, 'Success', class: "alert-#{l.type}")
442
260
  end
443
- end
444
- ```
445
-
446
- ```erb
447
- <%= component "button", label: "Sign up", url: sign_up_path, context: "primary" %>
448
- <%= component ButtonComponent, label: "Sign in", url: sign_in_path %>
449
- ```
450
261
 
451
- ### Namespaced components
452
-
453
- 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:
454
-
455
- ```ruby
456
- module Objects
457
- class MediaObject < Components::Component; end
458
262
  end
459
263
  ```
460
264
 
461
- Then call it from a template like so:
462
-
463
- ```erb
464
- <%= component "objects/media_object" %>
465
- ```
466
-
467
265
  ## Development
468
266
 
469
267
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -4,10 +4,9 @@ module Lite
4
4
  module Component
5
5
  module ComponentHelper
6
6
 
7
- def component(name, attrs = nil, &block)
7
+ def component(name, options = {})
8
8
  name = name.component_path if name.respond_to?(:component_path)
9
- klass = "#{name}_component".classify.constantize.new(self, attrs, &block)
10
- klass.render
9
+ "#{name}_component".classify.constantize.render(self, options)
11
10
  end
12
11
 
13
12
  end
@@ -0,0 +1,11 @@
1
+ Description:
2
+ Generate a component file
3
+
4
+ Example:
5
+ rails generate component NAME
6
+
7
+ This will create:
8
+ - app/assets/javascripts/components/[name].js
9
+ - app/assets/stylesheets/components/[name].scss
10
+ - app/components/[name]_query.rb
11
+ - app/views/components/_[name].html.erb
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+
5
+ module Rails
6
+ class ComponentGenerator < Rails::Generators::NamedBase
7
+
8
+ class_option :skip_erb, type: :boolean, default: false
9
+ class_option :skip_css, type: :boolean, default: false
10
+ class_option :skip_js, type: :boolean, default: false
11
+
12
+ source_root File.expand_path('../templates', __FILE__)
13
+ check_class_collision suffix: 'Component'
14
+
15
+ def create_component_file
16
+ template('install.rb.erb', "app/components/#{name}_component.rb")
17
+ end
18
+
19
+ def create_erb_file
20
+ return if options['skip_erb']
21
+
22
+ name_parts = name.split('/')
23
+ file_parts = name_parts[0..-2]
24
+ file_parts << "_#{name_parts.last}.html.erb"
25
+
26
+ create_file("app/views/components/#{file_parts.join('/')}")
27
+ end
28
+
29
+ def copy_javascript_file
30
+ return if options['skip_js']
31
+
32
+ copy_file('install.js', "app/assets/javascripts/components/#{name}.js")
33
+ end
34
+
35
+ def copy_stylesheet_file
36
+ return if options['skip_css']
37
+
38
+ copy_file('install.scss', "app/assets/stylesheets/components/#{name}.scss")
39
+ end
40
+
41
+ end
42
+ end