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.
- checksums.yaml +4 -4
- data/.solargraph.yml +1 -2
- data/CONTRIBUTING.md +2 -2
- data/Gemfile +1 -1
- data/Gemfile.lock +12 -12
- data/README.md +327 -4
- data/lib/amber_component/base.rb +31 -12
- data/lib/amber_component/helpers/component_helper.rb +1 -0
- data/lib/amber_component/minitest_test_case.rb +10 -0
- data/lib/amber_component/prop_definition.rb +54 -0
- data/lib/amber_component/props.rb +111 -0
- data/lib/amber_component/test_helper.rb +34 -0
- data/lib/amber_component/typed_content.rb +3 -3
- data/lib/amber_component/version.rb +1 -1
- data/lib/amber_component/views.rb +4 -4
- data/lib/amber_component.rb +8 -9
- data/lib/generators/amber_component/install_generator.rb +20 -1
- data/lib/generators/amber_component/templates/application_component_test_case.rb +7 -0
- data/lib/generators/amber_component_generator.rb +56 -4
- data/lib/generators/component_generator.rb +9 -0
- data/lib/generators/templates/component.rb.erb +3 -1
- data/lib/generators/templates/component_test.rb.erb +11 -3
- data/lib/generators/templates/style.css.erb +1 -1
- data/lib/generators/templates/style.sass.erb +3 -0
- data/lib/generators/templates/style.scss.erb +5 -0
- data/lib/generators/templates/view.haml.erb +9 -0
- data/lib/generators/templates/view.html.erb.erb +8 -0
- data/lib/generators/templates/view.slim.erb +6 -0
- metadata +13 -31
- data/docs/.bundle/config +0 -2
- data/docs/.gitignore +0 -5
- data/docs/404.html +0 -25
- data/docs/Gemfile +0 -37
- data/docs/Gemfile.lock +0 -89
- data/docs/README.md +0 -19
- data/docs/_config.yml +0 -148
- data/docs/_data/amber_component.yml +0 -3
- data/docs/_sass/_variables.scss +0 -2
- data/docs/_sass/color_schemes/amber_component.scss +0 -11
- data/docs/_sass/custom/custom.scss +0 -60
- data/docs/api/index.md +0 -8
- data/docs/assets/images/logo_wide.png +0 -0
- data/docs/changelog/index.md +0 -8
- data/docs/favicon.ico +0 -0
- data/docs/getting_started/index.md +0 -8
- data/docs/getting_started/installation.md +0 -7
- data/docs/getting_started/ruby_support.md +0 -7
- data/docs/getting_started/wireframes.md +0 -7
- data/docs/index.md +0 -17
- data/docs/introduction/basic_usage.md +0 -7
- data/docs/introduction/index.md +0 -8
- data/docs/introduction/why_amber_component.md +0 -7
- data/docs/resources/index.md +0 -8
- data/docs/styles/index.md +0 -8
- data/docs/styles/usage.md +0 -7
- data/docs/views/index.md +0 -8
- data/docs/views/usage.md +0 -7
- data/lib/generators/templates/view.html.erb +0 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f1809ef804fefeb8ed7e05b8d1056895f7068663b92b74b464b99bb611981e7c
|
4
|
+
data.tar.gz: 7a05630fb2c74850241378c1b2e176373258b0e3a63aa7eae26803fc0a7077d5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b554c4cbaafe3fd6e4cbebe34c4e99ea7d318e50aa38b599b87c35d72bf5a353e0099b0471d643543c6f6227275295f63c0f33432940d7d0de71b8ac29ddd488
|
7
|
+
data.tar.gz: 69ef4f133acd94753d9108b56ef44f601fd59435a53dadaf194905190f53db8dcc368dcd3c5da68cfcc027a8a27a4b6a33d99f40b3bb3985cf3c80ce9c79e8bc
|
data/.solargraph.yml
CHANGED
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:
|
75
|
+
> current directory: ~/.rvm/gems/ruby-3.1.0@dupa/gems/puma-5.6.2/ext/puma_http11
|
76
76
|
>
|
77
|
-
>
|
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
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
amber_component (0.0.
|
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.
|
70
|
+
nokogiri (1.13.9)
|
71
71
|
mini_portile2 (~> 2.8.0)
|
72
72
|
racc (~> 1.4)
|
73
|
-
nokogiri (1.13.
|
73
|
+
nokogiri (1.13.9-arm64-darwin)
|
74
74
|
racc (~> 1.4)
|
75
75
|
parallel (1.22.1)
|
76
|
-
parser (3.1.2.
|
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.
|
101
|
+
rubocop (1.36.0)
|
102
102
|
json (~> 2.3)
|
103
103
|
parallel (~> 1.10)
|
104
|
-
parser (>= 3.1.
|
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.
|
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.
|
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.
|
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.
|
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
|
171
|
+
solargraph
|
172
172
|
yard
|
173
173
|
|
174
174
|
BUNDLED WITH
|
175
|
-
2.3.
|
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
|
-
|
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
|
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).
|
data/lib/amber_component/base.rb
CHANGED
@@ -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
|
-
|
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
|
-
#
|
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,
|
93
|
-
|
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
|
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 `#{
|
100
|
-
Consider renaming this component, because the original method will be
|
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
|
-
|
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
|
137
|
+
def bind_instance_variables(kwargs)
|
119
138
|
kwargs.each do |key, value|
|
120
139
|
instance_variable_set("@#{key}", value)
|
121
140
|
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
|