amber_component 0.0.3 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.solargraph.yml +1 -2
  3. data/CONTRIBUTING.md +2 -2
  4. data/Gemfile +1 -1
  5. data/Gemfile.lock +12 -12
  6. data/README.md +327 -4
  7. data/lib/amber_component/base.rb +31 -12
  8. data/lib/amber_component/helpers/component_helper.rb +1 -0
  9. data/lib/amber_component/minitest_test_case.rb +10 -0
  10. data/lib/amber_component/prop_definition.rb +54 -0
  11. data/lib/amber_component/props.rb +111 -0
  12. data/lib/amber_component/test_helper.rb +34 -0
  13. data/lib/amber_component/typed_content.rb +3 -3
  14. data/lib/amber_component/version.rb +1 -1
  15. data/lib/amber_component/views.rb +4 -4
  16. data/lib/amber_component.rb +8 -9
  17. data/lib/generators/amber_component/install_generator.rb +20 -1
  18. data/lib/generators/amber_component/templates/application_component_test_case.rb +7 -0
  19. data/lib/generators/amber_component_generator.rb +56 -4
  20. data/lib/generators/component_generator.rb +9 -0
  21. data/lib/generators/templates/component.rb.erb +3 -1
  22. data/lib/generators/templates/component_test.rb.erb +11 -3
  23. data/lib/generators/templates/style.css.erb +1 -1
  24. data/lib/generators/templates/style.sass.erb +3 -0
  25. data/lib/generators/templates/style.scss.erb +5 -0
  26. data/lib/generators/templates/view.haml.erb +9 -0
  27. data/lib/generators/templates/view.html.erb.erb +8 -0
  28. data/lib/generators/templates/view.slim.erb +6 -0
  29. metadata +13 -31
  30. data/docs/.bundle/config +0 -2
  31. data/docs/.gitignore +0 -5
  32. data/docs/404.html +0 -25
  33. data/docs/Gemfile +0 -37
  34. data/docs/Gemfile.lock +0 -89
  35. data/docs/README.md +0 -19
  36. data/docs/_config.yml +0 -148
  37. data/docs/_data/amber_component.yml +0 -3
  38. data/docs/_sass/_variables.scss +0 -2
  39. data/docs/_sass/color_schemes/amber_component.scss +0 -11
  40. data/docs/_sass/custom/custom.scss +0 -60
  41. data/docs/api/index.md +0 -8
  42. data/docs/assets/images/logo_wide.png +0 -0
  43. data/docs/changelog/index.md +0 -8
  44. data/docs/favicon.ico +0 -0
  45. data/docs/getting_started/index.md +0 -8
  46. data/docs/getting_started/installation.md +0 -7
  47. data/docs/getting_started/ruby_support.md +0 -7
  48. data/docs/getting_started/wireframes.md +0 -7
  49. data/docs/index.md +0 -17
  50. data/docs/introduction/basic_usage.md +0 -7
  51. data/docs/introduction/index.md +0 -8
  52. data/docs/introduction/why_amber_component.md +0 -7
  53. data/docs/resources/index.md +0 -8
  54. data/docs/styles/index.md +0 -8
  55. data/docs/styles/usage.md +0 -7
  56. data/docs/views/index.md +0 -8
  57. data/docs/views/usage.md +0 -7
  58. data/lib/generators/templates/view.html.erb +0 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 239144479c990df66cda6d83bd062bdcfd0b0affca32597ebd968d490f6817d8
4
- data.tar.gz: 8d8cc882bceabcbff1cf16d0b5881274f55068a30b91acac98c2e108be248c0c
3
+ metadata.gz: f1809ef804fefeb8ed7e05b8d1056895f7068663b92b74b464b99bb611981e7c
4
+ data.tar.gz: 7a05630fb2c74850241378c1b2e176373258b0e3a63aa7eae26803fc0a7077d5
5
5
  SHA512:
6
- metadata.gz: f06b5e9288c557ae52015f637111ae6a6b90f0402a72b63873cdc1c3aae14d114c921bec304dda68b6ffddc4e4cc98e27d8cac28d84ec2758eb6da1d4176db28
7
- data.tar.gz: 8d950dcb6102a235f38078511705799a38956f94df12f8e5677f5d038c5adc598cf275ef2266c00f1ccfe9a54666793248afb076b982ea6858300692ce817776
6
+ metadata.gz: b554c4cbaafe3fd6e4cbebe34c4e99ea7d318e50aa38b599b87c35d72bf5a353e0099b0471d643543c6f6227275295f63c0f33432940d7d0de71b8ac29ddd488
7
+ data.tar.gz: 69ef4f133acd94753d9108b56ef44f601fd59435a53dadaf194905190f53db8dcc368dcd3c5da68cfcc027a8a27a4b6a33d99f40b3bb3985cf3c80ce9c79e8bc
data/.solargraph.yml CHANGED
@@ -9,8 +9,7 @@ exclude:
9
9
  require: []
10
10
  domains: []
11
11
  reporters:
12
- - rubocop
13
- - require_not_found
12
+ - typecheck:typed
14
13
  formatter:
15
14
  rubocop:
16
15
  cops: safe
data/CONTRIBUTING.md CHANGED
@@ -72,9 +72,9 @@ $ gem install ffi -- --with-cflags="-fdeclspec"
72
72
 
73
73
  > Gem::Ext::BuildError: ERROR: Failed to build gem native extension.
74
74
  >
75
- > current directory: /Users/mateuszdrewniak/.rvm/gems/ruby-3.1.0@dupa/gems/puma-5.6.2/ext/puma_http11
75
+ > current directory: ~/.rvm/gems/ruby-3.1.0@dupa/gems/puma-5.6.2/ext/puma_http11
76
76
  >
77
- > /Users/mateuszdrewniak/.rvm/rubies/ruby-3.1.0/bin/ruby -I /Users/mateuszdrewniak/.rvm/rubies/ruby-3.1.0/lib/ruby/3.1.0 -r ./siteconf20220219-40641-4uxhq6.rb extconf.rb --with-cflags\=-Wno-error\=implicit-function-declaration
77
+ > ~/.rvm/rubies/ruby-3.1.0/bin/ruby -I ~/.rvm/rubies/ruby-3.1.0/lib/ruby/3.1.0 -r ./siteconf20220219-40641-4uxhq6.rb extconf.rb --with-cflags\=-Wno-error\=implicit-function-declaration
78
78
  >
79
79
  > checking for BIO_read() in -lcrypto... *** extconf.rb failed ***
80
80
  >
data/Gemfile CHANGED
@@ -13,7 +13,7 @@ gem 'git'
13
13
  gem 'haml'
14
14
  gem 'rubocop', '~> 1.21'
15
15
  gem 'sassc'
16
- gem 'solargraph', '~> 0.45.0'
16
+ gem 'solargraph'
17
17
  gem 'yard'
18
18
 
19
19
  # Testing dependencies
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- amber_component (0.0.3)
4
+ amber_component (0.0.5)
5
5
  actionview (>= 6)
6
6
  activemodel (>= 6)
7
7
  activesupport (>= 6)
@@ -67,13 +67,13 @@ GEM
67
67
  method_source (1.0.0)
68
68
  mini_portile2 (2.8.0)
69
69
  minitest (5.16.2)
70
- nokogiri (1.13.8)
70
+ nokogiri (1.13.9)
71
71
  mini_portile2 (~> 2.8.0)
72
72
  racc (~> 1.4)
73
- nokogiri (1.13.8-arm64-darwin)
73
+ nokogiri (1.13.9-arm64-darwin)
74
74
  racc (~> 1.4)
75
75
  parallel (1.22.1)
76
- parser (3.1.2.0)
76
+ parser (3.1.2.1)
77
77
  ast (~> 2.4.1)
78
78
  racc (1.6.0)
79
79
  rack (2.2.4)
@@ -98,17 +98,17 @@ GEM
98
98
  reverse_markdown (2.1.1)
99
99
  nokogiri
100
100
  rexml (3.2.5)
101
- rubocop (1.32.0)
101
+ rubocop (1.36.0)
102
102
  json (~> 2.3)
103
103
  parallel (~> 1.10)
104
- parser (>= 3.1.0.0)
104
+ parser (>= 3.1.2.1)
105
105
  rainbow (>= 2.2.2, < 4.0)
106
106
  regexp_parser (>= 1.8, < 3.0)
107
107
  rexml (>= 3.2.5, < 4.0)
108
- rubocop-ast (>= 1.19.1, < 2.0)
108
+ rubocop-ast (>= 1.20.1, < 2.0)
109
109
  ruby-progressbar (~> 1.7)
110
110
  unicode-display_width (>= 1.4.0, < 3.0)
111
- rubocop-ast (1.19.1)
111
+ rubocop-ast (1.21.0)
112
112
  parser (>= 3.1.1.0)
113
113
  ruby-progressbar (1.11.0)
114
114
  ruby2_keywords (0.0.5)
@@ -124,7 +124,7 @@ GEM
124
124
  simplecov (~> 0.19)
125
125
  simplecov-html (0.12.3)
126
126
  simplecov_json_formatter (0.1.4)
127
- solargraph (0.45.0)
127
+ solargraph (0.46.0)
128
128
  backport (~> 1.2)
129
129
  benchmark
130
130
  bundler (>= 1.17.2)
@@ -144,7 +144,7 @@ GEM
144
144
  tilt (2.0.11)
145
145
  tzinfo (2.0.5)
146
146
  concurrent-ruby (~> 1.0)
147
- unicode-display_width (2.2.0)
147
+ unicode-display_width (2.3.0)
148
148
  webrick (1.7.0)
149
149
  yard (0.9.28)
150
150
  webrick (~> 1.7.0)
@@ -168,8 +168,8 @@ DEPENDENCIES
168
168
  shoulda-context (~> 2.0)
169
169
  simplecov
170
170
  simplecov-cobertura
171
- solargraph (~> 0.45.0)
171
+ solargraph
172
172
  yard
173
173
 
174
174
  BUNDLED WITH
175
- 2.3.14
175
+ 2.3.7
data/README.md CHANGED
@@ -9,13 +9,13 @@
9
9
 
10
10
  # AmberComponent
11
11
 
12
- AmberComponent is a simple component library which seamlessly hooks into your Rails project and allows you to create simple backend components. They work like mini controllers which are bound with their view.
12
+ AmberComponent is a simple component library which seamlessly hooks into your Rails project and allows you to create simple backend components. They work like mini controllers which are bound with their view and stylesheet.
13
13
 
14
14
  Created by [Garbus Beach](https://github.com/garbusbeach) and [Mateusz Drewniak](https://github.com/Verseth).
15
15
 
16
16
  ## Getting started
17
17
 
18
- You can read a lot more about AmberComponent in its [official docs](https://ambercomponent.com).
18
+ You can read a lot more about AmberComponent in its [official docs](https://ambercomponent.com).
19
19
 
20
20
  ## Installation
21
21
 
@@ -27,14 +27,337 @@ If bundler is not being used to manage dependencies, install the gem by executin
27
27
 
28
28
  $ gem install amber_component
29
29
 
30
+ If you're using a Rails application there's an installation generator that you should run:
31
+
32
+ ```sh
33
+ $ bin/rails generate amber_component:install
34
+ ```
35
+
30
36
  ## Usage
31
37
 
32
- If you're using a Rails application there's an installation generator that you should run:
38
+ ## Components
39
+
40
+ Components are located under `app/components`.
41
+
42
+ Every component consists of:
43
+ - a Ruby file which defines its properties, encapsulates logic and may implement helper methods (like a controller)
44
+ - a view/template file (html.erb, haml, slim etc.)
45
+ - a style file (css, scss, sass etc.)
46
+
47
+ An individual component which implements a button may look like this.
48
+
49
+ ```ruby
50
+ # app/components/button_component.rb
51
+
52
+ class ButtonComponent < AmberComponent::Base
53
+ prop :label, required: true
54
+ end
55
+ ```
56
+
57
+ ```html
58
+ <!-- app/components/button_component/view.html.erb -->
59
+
60
+ <div class="button_component">
61
+ <%= label %>
62
+ </div>
63
+ ```
64
+
65
+ ```scss
66
+ // app/components/button_component/style.scss
67
+
68
+ .button_component {
69
+ background-color: indigo;
70
+ border-radius: 1rem;
71
+ transition-duration: 500ms;
72
+
73
+ &:hover {
74
+ background-color: blue;
75
+ }
76
+ }
77
+ ```
78
+
79
+ You can render this component in other components or in a Rails view.
80
+
81
+ ```html
82
+ <!-- app/controller/foo/index.html.erb -->
83
+
84
+ <h1>We're inside FooController</h1>
85
+
86
+ <!-- using a helper method -->
87
+ <%= button_component label: 'Click me!' %>
88
+ <!-- calling a method on the component class -->
89
+ <%= ButtonComponent.call label: 'Click me!' %>
90
+ ```
91
+
92
+ Or even directly in Ruby
93
+
94
+ ```ruby
95
+ # Calling a method on the component class. Outputs an HTML string.
96
+ ButtonComponent.call label: 'Click me!'
97
+ #=> '<div class="button_component">Click me!</div>'
98
+ ```
99
+
100
+ ### Rails helpers inside component templates
101
+
102
+ Component views/template files can make use
103
+ of all ActionView helpers and Rails route helpers.
104
+
105
+ This makes component views very flexible and convenient.
106
+
107
+ ```erb
108
+ <!-- app/components/login_form_component/view.html.erb -->
109
+
110
+ <%= form_with url: sign_up_path, class: "login_form_component" do |f| %>
111
+ <%= f.label :first_name %>
112
+ <%= f.text_field :first_name %>
113
+
114
+ <%= f.label :last_name %>
115
+ <%= f.text_field :last_name %>
116
+
117
+ <%= f.label :email, "Email Address" %>
118
+ <%= f.text_field :email %>
119
+
120
+ <%= f.label :password %>
121
+ <%= f.password_field :password %>
122
+
123
+ <%= f.label :password_confirmation, "Confirm Password" %>
124
+ <%= f.password_field :password_confirmation %>
125
+
126
+ <%= f.submit "Create account" %>
127
+ <% end %>
128
+ ```
129
+
130
+ ### Component properties
131
+
132
+ There is a neat prop DSL.
133
+
134
+ ```ruby
135
+ # app/components/comment_component.rb
136
+
137
+ class CommentComponent < ApplicationComponent
138
+ # will raise an error when not present
139
+ prop :body, required: true
140
+ # will raise an error when an object of a different
141
+ # class is received (uses `is_a?`)
142
+ prop :author, type: User, allow_nil: true
143
+ # the default value
144
+ prop :date, default: -> { DateTime.now }
145
+ end
146
+ ```
147
+
148
+ Props can be passed as keyword arguments
149
+ to the `::call` method of the component class
150
+ or the helper method.
151
+
152
+ ```ruby
153
+ CommentComponent.call body: 'Foo bar', author: User.first
154
+ # only in views and other components
155
+ comment_component body: 'Foo bar', author: User.first
156
+ ```
157
+
158
+ ### Overriding prop getters and setters
159
+
160
+ Getters and setters for properties are
161
+ defined in a module which means that you can override them and call them with `super`.
162
+
163
+ ```ruby
164
+ # app/components/priority_icon_component.rb
165
+
166
+ class PriorityIconComponent < ApplicationComponent
167
+ PriorityStruct = Struct.new :icon, :color
168
+
169
+ PRIORITY_MAP = {
170
+ low: PriorityStruct.new('fa-solid fa-chevrons-down', 'green'),
171
+ medium: PriorityStruct.new('fa-solid fa-chevron-up', 'yellow'),
172
+ high: PriorityStruct.new('fa-solid fa-chevrons-up', 'red')
173
+ }
174
+
175
+ prop :severity, default: -> { :low }
176
+
177
+ def severity=(val)
178
+ # super will call the original
179
+ # implementation of the setter
180
+ super(PRIORITY_MAP[val])
181
+ end
182
+ end
183
+ ```
184
+
185
+ ```html
186
+ <!-- app/components/priority_icon_component/view.html.erb -->
187
+
188
+ <i style="color: <%= severity&.color %>;" class="<%= severity&.icon %>"></i>
189
+ ```
190
+
191
+ ### Helper methods
192
+
193
+ Defining helper methods which are available
194
+ in the template is extremely easy.
195
+
196
+ Just define a method on the component class.
197
+
198
+ ```ruby
199
+ # app/components/comment_component.rb
200
+
201
+ class CommentComponent < ApplicationComponent
202
+ # you can also include helper modules
203
+ include SomeHelper
204
+
205
+ prop :body, required: true
206
+ prop :author, type: Author, allow_nil: true
207
+ prop :date, default: -> { DateTime.now }
208
+
209
+ private
210
+
211
+ def humanized_date
212
+ date.strftime '%Y-%m-%d %H:%M'
213
+ end
214
+
215
+ def author_name
216
+ author&.name || 'Unknown'
217
+ end
218
+
219
+ def author_avatar
220
+ author&.avatar_url || User.placeholder_avatar_url
221
+ end
222
+ end
223
+ ```
224
+
225
+ ```html
226
+ <!-- app/components/comment_component/view.html.erb -->
227
+
228
+ <div class="comment_component">
229
+ <div class="comment_header">
230
+ <img src="<%= author_avatar %>" alt="<%= author_name %> avatar">
231
+
232
+ <div><%= author_name %></div>
233
+ <div class="comment_date"><%= humanized_date %></div>
234
+ </div>
235
+
236
+ <div class="comment_content">
237
+ <%= body %>
238
+ </div>
239
+ </div>
240
+ ```
241
+
242
+ ### Nested components
243
+
244
+ It's possible to nest components or provide
245
+ custom HTML to a component.
246
+
247
+ This works similarly to React's `props.children`.
248
+
249
+ To render the passed nested content call `yield.html_safe` somewhere inside the template/view.
250
+
251
+ ```ruby
252
+ # app/components/modal_component.rb
253
+
254
+ class ModalComponent < ApplicationComponent
255
+ prop :id, required: true
256
+ prop :title, required: true
257
+ end
258
+ ```
259
+
260
+ ```html
261
+ <!-- app/components/modal/view.html.erb -->
262
+
263
+ <div id="<%= id %>" class="modal_component">
264
+ <div class="model_header">
265
+ <%= title %>
266
+ </div>
267
+
268
+ <div class="modal_body">
269
+ <!-- nested content will be rendered here -->
270
+ <%= yield.html_safe %>
271
+ </div>
272
+
273
+ <div class="modal_footer">
274
+ <div class="modal_close_button"></div>
275
+ </div>
276
+ <div>
277
+ ```
278
+
279
+ You can pass a body to this modal by passing
280
+ a block.
281
+
282
+ ```erb
283
+ <!-- app/controller/tasks/show.html.erb -->
284
+
285
+ <%= ModalComponent.call id: 'update-task-modal' title: 'Update the task' do %>
286
+ <h2>This is your task!</h2>
287
+ <%= form_with model: @task do |f| %>
288
+ <%= f.text_field :name %>
289
+ <%= f.text_area :description %>
290
+ <%= f.submit %>
291
+ <% end %>
292
+ <% end %>
293
+ ```
294
+
295
+ Note that this will raise an error when no block/nested content is provided.
296
+
297
+ In order to render nested content
298
+ only when it is present (will work without nested content)
299
+ you can use `yield.html_safe if block_given?`
300
+
301
+ In general `block_given?` will return `true` when a block/nested content is present, otherwise `false`.
302
+
303
+ ### Components with namespaces
304
+
305
+ Components may be defined inside multiple modules/namespaces.
306
+
307
+ ```ruby
308
+ # app/components/sign_up/button_component.rb
309
+
310
+ class SignUp::ButtonComponent < AmberComponent::Base
311
+ prop :label, required: true
312
+ end
313
+ ```
314
+
315
+ ```html
316
+ <!-- app/components/sign_up/button_component/view.html.erb -->
317
+
318
+ <div class="sign_up_button_component">
319
+ <%= label %>
320
+ </div>
321
+ ```
322
+
323
+ ```scss
324
+ // app/components/sign_up/button_component/style.scss
325
+
326
+ .sign_up_button_component {
327
+ background-color: indigo;
328
+ border-radius: 1rem;
329
+ transition-duration: 500ms;
330
+
331
+ &:hover {
332
+ background-color: blue;
333
+ }
334
+ }
335
+ ```
336
+
337
+ You can render such a component by calling the `::call` method
338
+ on its class, or by using the helper method defined on its parent module.
339
+
340
+ ```ruby
341
+ SignUp::ButtonComponent.call label: 'Sign up!'
342
+ SignUp.button_component label: 'Sign up!'
343
+ ```
344
+
345
+ ### Generating Components
346
+
347
+ You an generate new components by running
33
348
 
34
349
  ```sh
35
- $ rails generate amber_component:install
350
+ $ bin/rails generate component foo_bar
36
351
  ```
37
352
 
353
+ or
354
+
355
+ ```sh
356
+ $ bin/rails generate component FooBar
357
+ ```
358
+
359
+ This will generate a new component in `app/components/foo_bar_component.rb` along with a view, stylesheet and test file.
360
+
38
361
  ## Contribute
39
362
 
40
363
  Do you want to contribute to AmberComponent? Open the issues page and check for the help wanted label! But before you start coding, please read our [Contributing Guide](https://github.com/amber-ruby/amber_component/blob/main/CONTRIBUTING.md).
@@ -48,6 +48,8 @@ module ::AmberComponent
48
48
  extend Assets::ClassMethods
49
49
  include Rendering::InstanceMethods
50
50
  extend Rendering::ClassMethods
51
+ include Props::InstanceMethods
52
+ extend Props::ClassMethods
51
53
 
52
54
  class << self
53
55
  include ::Memery
@@ -67,37 +69,51 @@ module ::AmberComponent
67
69
  # @return [void]
68
70
  def inherited(subclass)
69
71
  super
70
- # @type [Module]
71
- method_body = proc do |**kwargs, &block|
72
+ method_body = lambda do |**kwargs, &block|
72
73
  subclass.render(**kwargs, &block)
73
74
  end
74
75
  parent_module = subclass.module_parent
75
76
 
76
77
  if parent_module.equal?(::Object)
77
78
  method_name = subclass.name
78
- define_helper_method(subclass, Helpers::ComponentHelper, method_name, method_body)
79
79
  define_helper_method(subclass, Helpers::ComponentHelper, method_name.underscore, method_body)
80
80
  return
81
81
  end
82
82
 
83
83
  method_name = subclass.const_name
84
- define_helper_method(subclass, parent_module.singleton_class, method_name, method_body)
85
84
  define_helper_method(subclass, parent_module.singleton_class, method_name.underscore, method_body)
86
85
  end
87
86
 
88
- # @param component [Class]
87
+ # Gets or defines an anonymous module that
88
+ # will store all dynamically generated helper methods
89
+ # for the received module/class.
90
+ #
89
91
  # @param mod [Module, Class]
92
+ # @return [Module]
93
+ def helper_module(mod)
94
+ ivar_name = :@__amber_component_helper_module
95
+ mod.instance_variable_get(ivar_name)&.then { return _1 }
96
+
97
+ helper_mod = mod.instance_variable_set(ivar_name, ::Module.new)
98
+ mod.include helper_mod
99
+ helper_mod
100
+ end
101
+
102
+ # Defines an instance method on the given `mod` Module/Class.
103
+ #
104
+ # @param component [Class]
105
+ # @param target_mod [Module, Class]
90
106
  # @param method_name [String, Symbol]
91
107
  # @param body [Proc]
92
- def define_helper_method(component, mod, method_name, body)
93
- mod.define_method(method_name, &body)
108
+ def define_helper_method(component, target_mod, method_name, body)
109
+ helper_module(target_mod).define_method(method_name, &body)
94
110
 
95
111
  return if ::ENV['RAILS_ENV'] == 'production'
96
112
 
97
- ::Warning.warn <<~WARN if mod.instance_methods.include?(method_name)
113
+ ::Warning.warn <<~WARN if target_mod.instance_methods.include?(method_name)
98
114
  #{caller(0, 1).first}: warning:
99
- `#{component}` shadows the name of an already existing `#{mod}` method `#{method_name}`.
100
- Consider renaming this component, because the original method will be overwritten.
115
+ `#{component}` shadows the name of an already existing `#{target_mod}` method `#{method_name}`.
116
+ Consider renaming this component, because the original method will be overridden.
101
117
  WARN
102
118
  end
103
119
  end
@@ -105,9 +121,12 @@ module ::AmberComponent
105
121
  define_model_callbacks :initialize, :render
106
122
 
107
123
  # @param kwargs [Hash{Symbol => Object}]
124
+ # @raise [AmberComponent::MissingPropsError] when required props are missing
108
125
  def initialize(**kwargs)
109
126
  run_callbacks :initialize do
110
- bind_variables(kwargs)
127
+ next if bind_props(kwargs)
128
+
129
+ bind_instance_variables(kwargs)
111
130
  end
112
131
  end
113
132
 
@@ -115,7 +134,7 @@ module ::AmberComponent
115
134
 
116
135
  # @param kwargs [Hash{Symbol => Object}]
117
136
  # @return [void]
118
- def bind_variables(kwargs)
137
+ def bind_instance_variables(kwargs)
119
138
  kwargs.each do |key, value|
120
139
  instance_variable_set("@#{key}", value)
121
140
  end
@@ -7,6 +7,7 @@ module ::AmberComponent
7
7
  # Contains methods for quickly rendering
8
8
  # components defined under the root namespace `Object`.
9
9
  module ComponentHelper
10
+ @__amber_component_helper_module = self
10
11
  end
11
12
  end
12
13
  end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'test_helper'
4
+
5
+ module AmberComponent
6
+ # A base class for component tests with Minitest
7
+ class MinitestTestCase < ::Minitest::Test
8
+ include TestHelper
9
+ end
10
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ::AmberComponent
4
+ # Internal class which represents a property
5
+ # on a component class.
6
+ class PropDefinition
7
+ # @return [Symbol]
8
+ attr_reader :name
9
+ # @return [Class, nil]
10
+ attr_reader :type
11
+ # @return [Boolean]
12
+ attr_reader :required
13
+ # @return [Object, Proc, nil]
14
+ attr_reader :default
15
+ # @return [Boolean]
16
+ attr_reader :allow_nil
17
+
18
+ # @param name [Symbol]
19
+ # @param type [Class, nil]
20
+ # @param required [Boolean]
21
+ # @param default [Object, Proc, nil]
22
+ # @param allow_nil [Boolean]
23
+ def initialize(name:, type: nil, required: false, default: nil, allow_nil: false)
24
+ @name = name
25
+ @type = type
26
+ @required = required
27
+ @default = default
28
+ @allow_nil = allow_nil
29
+ end
30
+
31
+ alias required? required
32
+ alias allow_nil? allow_nil
33
+
34
+ # @return [Boolean]
35
+ def type?
36
+ !@type.nil?
37
+ end
38
+
39
+ # @return [Boolean]
40
+ def default?
41
+ !@default.nil?
42
+ end
43
+
44
+ # Evaluate the default value if it's a `Proc`
45
+ # and return the result.
46
+ #
47
+ # @return [Object]
48
+ def default!
49
+ return @default.call if @default.is_a?(::Proc)
50
+
51
+ @default
52
+ end
53
+ end
54
+ end