amber_component 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.kanbn/index.md +23 -0
- data/.kanbn/tasks/add-instance-variables-to-view-when-block-given-markdown.md +9 -0
- data/.kanbn/tasks/bind-clas-to-action-view-method-call-example-component-data-data-without-calling-any-method.md +9 -0
- data/.kanbn/tasks/bind-scoped-css-to-head-of-doc.md +10 -0
- data/.kanbn/tasks/check-if-we-need-full-rails-gem-pack.md +9 -0
- data/.kanbn/tasks/simple-proto.md +10 -0
- data/.kanbn/tasks/verify-if-template-is-haml-or-erb.md +9 -0
- data/.rubocop.yml +184 -0
- data/.ruby-version +1 -0
- data/.solargraph.yml +22 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +22 -0
- data/Gemfile.lock +250 -0
- data/LICENSE +21 -0
- data/PLANS.md +17 -0
- data/README.md +63 -0
- data/Rakefile +18 -0
- data/amber_component.gemspec +47 -0
- data/lib/amber_component/base.rb +492 -0
- data/lib/amber_component/helper.rb +8 -0
- data/lib/amber_component/style_injector.rb +52 -0
- data/lib/amber_component/typed_content.rb +46 -0
- data/lib/amber_component/version.rb +5 -0
- data/lib/amber_component.rb +24 -0
- data/sig/amber_components.rbs +4 -0
- metadata +120 -0
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/gem_tasks'
|
4
|
+
require 'rake/testtask'
|
5
|
+
|
6
|
+
::Rake::TestTask.new(:test) do |t|
|
7
|
+
t.libs << 'lib'
|
8
|
+
t.libs << 'test'
|
9
|
+
t.libs << 'test/fixtures'
|
10
|
+
# ignore tests of rails apps
|
11
|
+
t.test_files = ::FileList['test/**/*_test.rb'] - ::FileList['test/apps/**/*_test.rb']
|
12
|
+
end
|
13
|
+
|
14
|
+
require 'rubocop/rake_task'
|
15
|
+
|
16
|
+
::RuboCop::RakeTask.new
|
17
|
+
|
18
|
+
task default: %i[test rubocop]
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/amber_component/version"
|
4
|
+
|
5
|
+
::Gem::Specification.new do |spec|
|
6
|
+
spec.name = "amber_component"
|
7
|
+
spec.version = ::AmberComponent::VERSION
|
8
|
+
spec.authors = ['Ruby-Amber', 'Mateusz Drewniak', 'Garbus Beach']
|
9
|
+
spec.email = ['matmg24@gmail.com', 'piotr.garbus.garbicz@gmail.com']
|
10
|
+
|
11
|
+
spec.summary = "A simple component library which seamlessly hooks into your Rails project."
|
12
|
+
spec.description = <<~DESC
|
13
|
+
A simple component library which seamlessly hooks into your Rails project
|
14
|
+
and allows you to create simple backend components.
|
15
|
+
|
16
|
+
They work like mini controllers which are bound with their view.
|
17
|
+
DESC
|
18
|
+
spec.homepage = 'https://github.com/amber-ruby/amber_component'
|
19
|
+
spec.license = 'MIT'
|
20
|
+
spec.required_ruby_version = ">= 2.6.0"
|
21
|
+
|
22
|
+
# spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"
|
23
|
+
|
24
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
25
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
26
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
27
|
+
# spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
|
28
|
+
|
29
|
+
# Specify which files should be added to the gem when it is released.
|
30
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
31
|
+
spec.files = ::Dir.chdir(__dir__) do
|
32
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
33
|
+
(f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
|
34
|
+
end
|
35
|
+
end
|
36
|
+
spec.bindir = "exe"
|
37
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| ::File.basename(f) }
|
38
|
+
spec.require_paths = ["lib"]
|
39
|
+
|
40
|
+
# Uncomment to register a new dependency of your gem
|
41
|
+
spec.add_dependency "memery", ">= 1.4.1"
|
42
|
+
spec.add_dependency "rails", ">= 6"
|
43
|
+
spec.add_dependency "tilt", ">= 2.0.10"
|
44
|
+
|
45
|
+
# For more information and examples about making a new gem, check out our
|
46
|
+
# guide at: https://bundler.io/guides/creating_gem.html
|
47
|
+
end
|
@@ -0,0 +1,492 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'set'
|
4
|
+
require 'erb'
|
5
|
+
require 'tilt'
|
6
|
+
require 'active_model/callbacks'
|
7
|
+
require 'memery'
|
8
|
+
|
9
|
+
require_relative './style_injector'
|
10
|
+
|
11
|
+
# Abstract class which serves as a base
|
12
|
+
# for all Amber Components.
|
13
|
+
#
|
14
|
+
# There are a few life cycle callbacks that can be defined.
|
15
|
+
# The same way as in `ActiveRecord` models and `ActionController` controllers.
|
16
|
+
#
|
17
|
+
# - before_render
|
18
|
+
# - around_render
|
19
|
+
# - after_render
|
20
|
+
# - before_initialize
|
21
|
+
# - around_initialize
|
22
|
+
# - after_initialize
|
23
|
+
#
|
24
|
+
# class ButtonComponent < ::AmberComponent::Base
|
25
|
+
# # You can provide a Symbol of the method that should be called
|
26
|
+
# before_render :before_render_method
|
27
|
+
# # Or provide a block that will be executed
|
28
|
+
# after_initialize do
|
29
|
+
# # Your code here
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# def before_render_method
|
33
|
+
# # Your code here
|
34
|
+
# end
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
#
|
38
|
+
# @abstract Create a subclass to define a new component.
|
39
|
+
module ::AmberComponent
|
40
|
+
class Base # :nodoc:
|
41
|
+
extend ::ActiveModel::Callbacks
|
42
|
+
|
43
|
+
include Helper
|
44
|
+
|
45
|
+
# @return [Regexp]
|
46
|
+
VIEW_FILE_REGEXP = /^view\./.freeze
|
47
|
+
# @return [Regexp]
|
48
|
+
STYLE_FILE_REGEXP = /^style\./.freeze
|
49
|
+
|
50
|
+
# View types with built-in embedded Ruby
|
51
|
+
#
|
52
|
+
# @return [Set<Symbol>]
|
53
|
+
VIEW_TYPES_WITH_RUBY = ::Set[:erb, :haml, :slim].freeze
|
54
|
+
# @return [Set<Symbol>]
|
55
|
+
ALLOWED_VIEW_TYPES = ::Set[:erb, :haml, :slim, :html, :md, :markdown].freeze
|
56
|
+
# @return [Set<Symbol>]
|
57
|
+
ALLOWED_STYLE_TYPES = ::Set[:sass, :scss, :less].freeze
|
58
|
+
|
59
|
+
class << self
|
60
|
+
include ::Memery
|
61
|
+
|
62
|
+
# @param kwargs [Hash{Symbol => Object}]
|
63
|
+
# @return [String]
|
64
|
+
def run(**kwargs, &block)
|
65
|
+
comp = new(**kwargs)
|
66
|
+
|
67
|
+
comp.render(&block)
|
68
|
+
end
|
69
|
+
|
70
|
+
alias call run
|
71
|
+
|
72
|
+
|
73
|
+
# @return [String]
|
74
|
+
memoize def asset_dir_path
|
75
|
+
class_const_name = name.split('::').last
|
76
|
+
component_file_path, = module_parent.const_source_location(class_const_name)
|
77
|
+
|
78
|
+
component_file_path.delete_suffix('.rb')
|
79
|
+
end
|
80
|
+
|
81
|
+
# @return [String]
|
82
|
+
def view_path
|
83
|
+
asset_path view_file_name
|
84
|
+
end
|
85
|
+
|
86
|
+
# @return [String, nil]
|
87
|
+
def view_file_name
|
88
|
+
files = asset_file_names(VIEW_FILE_REGEXP)
|
89
|
+
raise MultipleViews, "More than one view file for `#{name}` found!" if files.length > 1
|
90
|
+
|
91
|
+
files.first
|
92
|
+
end
|
93
|
+
|
94
|
+
# @return [Symbol]
|
95
|
+
def view_type
|
96
|
+
(view_file_name.split('.')[1..].reject { _1.match?(/erb/) }.last || 'erb')&.to_sym
|
97
|
+
end
|
98
|
+
|
99
|
+
# @return [String]
|
100
|
+
def style_path
|
101
|
+
asset_path style_file_name
|
102
|
+
end
|
103
|
+
|
104
|
+
# @return [String, nil]
|
105
|
+
def style_file_name
|
106
|
+
files = asset_file_names(STYLE_FILE_REGEXP)
|
107
|
+
raise MultipleStyles, "More than one style file for `#{name}` found!" if files.length > 1
|
108
|
+
|
109
|
+
files.first
|
110
|
+
end
|
111
|
+
|
112
|
+
# @return [Symbol]
|
113
|
+
def style_type
|
114
|
+
(style_file_name.split('.')[1..].reject { _1.match?(/erb/) }.last || 'erb')&.to_sym
|
115
|
+
end
|
116
|
+
|
117
|
+
# Memoize these methods in production
|
118
|
+
if defined?(::Rails) && ::Rails.env.production?
|
119
|
+
memoize :view_path
|
120
|
+
memoize :view_file_name
|
121
|
+
memoize :view_type
|
122
|
+
|
123
|
+
memoize :style_path
|
124
|
+
memoize :style_file_name
|
125
|
+
memoize :style_type
|
126
|
+
end
|
127
|
+
|
128
|
+
# Register an inline view by returning a String from the passed block.
|
129
|
+
#
|
130
|
+
# Usage:
|
131
|
+
#
|
132
|
+
# view do
|
133
|
+
# <<~ERB
|
134
|
+
# <h1>
|
135
|
+
# Hello <%= @name %>
|
136
|
+
# </h1>
|
137
|
+
# ERB
|
138
|
+
# end
|
139
|
+
#
|
140
|
+
# or:
|
141
|
+
#
|
142
|
+
# view :haml do
|
143
|
+
# <<~HAML
|
144
|
+
# %h1
|
145
|
+
# Hello
|
146
|
+
# = @name
|
147
|
+
# HAML
|
148
|
+
# end
|
149
|
+
#
|
150
|
+
# @param type [Symbol]
|
151
|
+
# @return [void]
|
152
|
+
def view(type = :erb, &block)
|
153
|
+
@method_view = TypedContent.new(type: type, content: block)
|
154
|
+
end
|
155
|
+
|
156
|
+
# ERB/Haml/Slim view registered through the `view` method.
|
157
|
+
#
|
158
|
+
# @return [TypedContent]
|
159
|
+
attr_reader :method_view
|
160
|
+
|
161
|
+
# Register an inline style by returning a String from the passed block.
|
162
|
+
#
|
163
|
+
# Usage:
|
164
|
+
#
|
165
|
+
# style do
|
166
|
+
# '.my-class { color: red; }'
|
167
|
+
# end
|
168
|
+
#
|
169
|
+
# or:
|
170
|
+
#
|
171
|
+
# style :sass do
|
172
|
+
# <<~SASS
|
173
|
+
# .my-class
|
174
|
+
# color: red
|
175
|
+
# SASS
|
176
|
+
# end
|
177
|
+
#
|
178
|
+
# @param type [Symbol]
|
179
|
+
# @return [void]
|
180
|
+
def style(type = :css, &block)
|
181
|
+
@method_style = TypedContent.new(type: type, content: block)
|
182
|
+
end
|
183
|
+
|
184
|
+
# CSS/SCSS/Sass styles registered through the `style` method.
|
185
|
+
#
|
186
|
+
# @return [TypedContent]
|
187
|
+
attr_reader :method_style
|
188
|
+
|
189
|
+
private
|
190
|
+
|
191
|
+
# @param subclass [Class]
|
192
|
+
# @return [void]
|
193
|
+
def inherited(subclass)
|
194
|
+
# @type [Module]
|
195
|
+
parent_module = subclass.module_parent
|
196
|
+
method_body = proc do |**kwargs, &block|
|
197
|
+
subclass.run(**kwargs, &block)
|
198
|
+
end
|
199
|
+
|
200
|
+
Helper.define_method(subclass.name, &method_body) && return if parent_module.equal? ::Object
|
201
|
+
|
202
|
+
parent_module.define_singleton_method(subclass.name.split('::').last, &method_body)
|
203
|
+
end
|
204
|
+
|
205
|
+
# @param file_name [String, nil]
|
206
|
+
# @return [String, nil]
|
207
|
+
def asset_path(file_name)
|
208
|
+
return unless file_name
|
209
|
+
|
210
|
+
::File.join(asset_dir_path, file_name)
|
211
|
+
end
|
212
|
+
|
213
|
+
# Returns the name of the file inside the asset directory
|
214
|
+
# of this component that matches the provided `Regexp`
|
215
|
+
#
|
216
|
+
# @param type_regexp [Regexp]
|
217
|
+
# @return [Array<String>]
|
218
|
+
def asset_file_names(type_regexp)
|
219
|
+
return [] unless ::File.directory?(asset_dir_path)
|
220
|
+
|
221
|
+
::Dir.entries(asset_dir_path).select do |file|
|
222
|
+
next unless ::File.file?(::File.join(asset_dir_path, file))
|
223
|
+
|
224
|
+
file.match? type_regexp
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
define_model_callbacks :initialize, :render
|
230
|
+
|
231
|
+
# @param kwargs [Hash{Symbol => Object}]
|
232
|
+
def initialize(**kwargs)
|
233
|
+
run_callbacks :initialize do
|
234
|
+
bind_variables(kwargs)
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
# @return [String]
|
239
|
+
def render(&block)
|
240
|
+
run_callbacks :render do
|
241
|
+
element = inject_views(&block)
|
242
|
+
styles = inject_styles
|
243
|
+
element += styles unless styles.nil?
|
244
|
+
element.html_safe
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
def render_in(context)
|
249
|
+
byebug
|
250
|
+
end
|
251
|
+
|
252
|
+
private
|
253
|
+
|
254
|
+
# @param kwargs [Hash{Symbol => Object}]
|
255
|
+
# @return [void]
|
256
|
+
def bind_variables(kwargs)
|
257
|
+
kwargs.each do |key, value|
|
258
|
+
instance_variable_set("@#{key}", value)
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
# Helper method to render view from string or with other provided type.
|
263
|
+
#
|
264
|
+
# Usage:
|
265
|
+
#
|
266
|
+
# render_custom_view('<h1>Hello World</h1>')
|
267
|
+
#
|
268
|
+
# or:
|
269
|
+
#
|
270
|
+
# render_custom_view content: '**Hello World**', type: 'md'
|
271
|
+
#
|
272
|
+
# @param style [TypedContent, Hash{Symbol => String, Symbol, Proc}, String]
|
273
|
+
# @return [String, nil]
|
274
|
+
def render_custom_view(view, &block)
|
275
|
+
return '' unless view
|
276
|
+
return view if view.is_a?(::String)
|
277
|
+
|
278
|
+
view = TypedContent.wrap(view)
|
279
|
+
type = view.type
|
280
|
+
content = view.to_s
|
281
|
+
|
282
|
+
if content.empty?
|
283
|
+
raise EmptyView, <<~ERR.squish
|
284
|
+
Custom view for `#{self.class}` from view method cannot be empty!
|
285
|
+
ERR
|
286
|
+
end
|
287
|
+
|
288
|
+
unless ALLOWED_VIEW_TYPES.include? type
|
289
|
+
raise UnknownViewType, <<~ERR.squish
|
290
|
+
Unknown view type for `#{self.class}` from view method!
|
291
|
+
Check return value of param type in `view :[type] do`
|
292
|
+
ERR
|
293
|
+
end
|
294
|
+
|
295
|
+
unless VIEW_TYPES_WITH_RUBY.include? type
|
296
|
+
# first render the content with ERB if the
|
297
|
+
# type does not support embedding Ruby by default
|
298
|
+
content = render_string(content, :erb, block)
|
299
|
+
end
|
300
|
+
|
301
|
+
render_string(content, type, block)
|
302
|
+
end
|
303
|
+
|
304
|
+
# @return [String]
|
305
|
+
def render_view_from_file(&block)
|
306
|
+
view_path = self.class.view_path
|
307
|
+
return '' if view_path.nil? || !::File.file?(view_path)
|
308
|
+
|
309
|
+
content = ::File.read(view_path)
|
310
|
+
type = self.class.view_type
|
311
|
+
|
312
|
+
unless VIEW_TYPES_WITH_RUBY.include? type
|
313
|
+
content = render_string(content, :erb, block)
|
314
|
+
end
|
315
|
+
|
316
|
+
render_string(content, type, block)
|
317
|
+
end
|
318
|
+
|
319
|
+
# Method returning view from method in class file.
|
320
|
+
# Usage:
|
321
|
+
#
|
322
|
+
# view do
|
323
|
+
# <<~HTML
|
324
|
+
# <h1>
|
325
|
+
# Hello <%= @name %>
|
326
|
+
# </h1>
|
327
|
+
# HTML
|
328
|
+
# end
|
329
|
+
#
|
330
|
+
# or:
|
331
|
+
#
|
332
|
+
# view :haml do
|
333
|
+
# <<~HAML
|
334
|
+
# %h1
|
335
|
+
# Hello
|
336
|
+
# = @name
|
337
|
+
# HAML
|
338
|
+
# end
|
339
|
+
#
|
340
|
+
# @return [String]
|
341
|
+
def render_class_method_view(&block)
|
342
|
+
render_custom_view(self.class.method_view, &block)
|
343
|
+
end
|
344
|
+
|
345
|
+
# Method returning view from params in view.
|
346
|
+
# Usage:
|
347
|
+
#
|
348
|
+
# <%= ExampleComponent data: data, view: "<h1>Hello #{@name}</h1>" %>
|
349
|
+
#
|
350
|
+
# or:
|
351
|
+
#
|
352
|
+
# <%= ExampleComponent data: data, view: { content: "<h1>Hello #{@name}</h1>", type: 'erb' } %>
|
353
|
+
#
|
354
|
+
# @return [String]
|
355
|
+
def render_view_from_inline(&block)
|
356
|
+
data = \
|
357
|
+
if @view.is_a? String
|
358
|
+
TypedContent.new(
|
359
|
+
type: :erb,
|
360
|
+
content: @view
|
361
|
+
)
|
362
|
+
else
|
363
|
+
@view
|
364
|
+
end
|
365
|
+
|
366
|
+
render_custom_view(data, &block)
|
367
|
+
end
|
368
|
+
|
369
|
+
# @return [String]
|
370
|
+
def inject_views(&block)
|
371
|
+
view_from_file = render_view_from_file(&block)
|
372
|
+
view_from_method = render_class_method_view(&block)
|
373
|
+
view_from_inline = render_view_from_inline(&block)
|
374
|
+
|
375
|
+
view_content = view_from_file unless view_from_file.empty?
|
376
|
+
view_content = view_from_method unless view_from_method.empty?
|
377
|
+
view_content = view_from_inline unless view_from_inline.empty?
|
378
|
+
|
379
|
+
if view_content.nil? || view_content.empty?
|
380
|
+
raise ViewFileNotFound, "View for `#{self.class}` could not be found!"
|
381
|
+
end
|
382
|
+
|
383
|
+
view_content
|
384
|
+
end
|
385
|
+
|
386
|
+
# Helper method to render style from css string or with other provided type.
|
387
|
+
#
|
388
|
+
# Usage:
|
389
|
+
#
|
390
|
+
# render_custom_style('.my-class { color: red; }')
|
391
|
+
#
|
392
|
+
# or:
|
393
|
+
#
|
394
|
+
# render_custom_style style: '.my-class { color: red; }', type: 'sass'
|
395
|
+
#
|
396
|
+
# @param style [TypedContent, Hash{Symbol => Symbol, String, Proc}, String]
|
397
|
+
# @return [String, nil]
|
398
|
+
def render_custom_style(style)
|
399
|
+
return '' unless style
|
400
|
+
return style if style.is_a?(::String)
|
401
|
+
|
402
|
+
style = TypedContent.wrap(style)
|
403
|
+
type = style.type
|
404
|
+
content = style.to_s
|
405
|
+
|
406
|
+
if content.empty?
|
407
|
+
raise EmptyStyle, <<~ERR.squish
|
408
|
+
Custom style for `#{self.class}` from style method cannot be empty!
|
409
|
+
ERR
|
410
|
+
end
|
411
|
+
|
412
|
+
unless ALLOWED_STYLE_TYPES.include? type
|
413
|
+
raise UnknownStyleType, <<~ERR.squish
|
414
|
+
Unknown style type for `#{self.class}` from style method!
|
415
|
+
Check return value of param type in `style :[type] do`
|
416
|
+
ERR
|
417
|
+
end
|
418
|
+
|
419
|
+
# first render the content with ERB
|
420
|
+
content = render_string(content, :erb)
|
421
|
+
|
422
|
+
render_string(content, type)
|
423
|
+
end
|
424
|
+
|
425
|
+
# Method returning style from file (style.(css|sass|scss|less)) if exists.
|
426
|
+
#
|
427
|
+
# @return [String]
|
428
|
+
def render_style_from_file
|
429
|
+
style_path = self.class.style_path
|
430
|
+
return '' unless style_path
|
431
|
+
|
432
|
+
content = ::File.read(style_path)
|
433
|
+
type = self.class.style_type
|
434
|
+
|
435
|
+
return content if type == :css
|
436
|
+
|
437
|
+
content = render_string(content, :erb)
|
438
|
+
render_string(content, type)
|
439
|
+
end
|
440
|
+
|
441
|
+
# Method returning style from method in class file.
|
442
|
+
# Usage:
|
443
|
+
#
|
444
|
+
# style do
|
445
|
+
# '.my-class { color: red; }'
|
446
|
+
# end
|
447
|
+
#
|
448
|
+
# or:
|
449
|
+
#
|
450
|
+
# style :sass do
|
451
|
+
# <<~SASS
|
452
|
+
# .my-class
|
453
|
+
# color: red
|
454
|
+
# SASS
|
455
|
+
# end
|
456
|
+
#
|
457
|
+
# @return [String]
|
458
|
+
def render_class_method_style
|
459
|
+
render_custom_style(self.class.method_style)
|
460
|
+
end
|
461
|
+
|
462
|
+
# Method returning style from params in view.
|
463
|
+
# Usage:
|
464
|
+
#
|
465
|
+
# <%= ExampleComponent data: data, style: '.my-class { color: red; }' %>
|
466
|
+
#
|
467
|
+
# or:
|
468
|
+
#
|
469
|
+
# <%= ExampleComponent data: data, style: {style: '.my-class { color: red; }', type: 'sass'} %>
|
470
|
+
#
|
471
|
+
# @return [String]
|
472
|
+
def render_style_from_inline
|
473
|
+
render_custom_style(@style)
|
474
|
+
end
|
475
|
+
|
476
|
+
# @param content [String]
|
477
|
+
# @param type [Symbol]
|
478
|
+
# @param block [Proc, nil]
|
479
|
+
# @return [String]
|
480
|
+
def render_string(content, type, block = nil)
|
481
|
+
::Tilt[type].new { content }.render(self, &block)
|
482
|
+
end
|
483
|
+
|
484
|
+
# @return [String]
|
485
|
+
def inject_styles
|
486
|
+
style_content = render_style_from_file + render_class_method_style + render_style_from_inline
|
487
|
+
return if style_content.empty?
|
488
|
+
|
489
|
+
::AmberComponent::StyleInjector.inject(style_content)
|
490
|
+
end
|
491
|
+
end
|
492
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'nokogiri'
|
4
|
+
|
5
|
+
# Helper class for injecting styles into DOM.
|
6
|
+
class ::AmberComponent::StyleInjector
|
7
|
+
class << self
|
8
|
+
|
9
|
+
# Injects styles into the DOM, or returns string if
|
10
|
+
# DOM stucture is not available.
|
11
|
+
#
|
12
|
+
# @param style [String]
|
13
|
+
# @return [String, nil]
|
14
|
+
def inject(style)
|
15
|
+
injector = new(style)
|
16
|
+
injector.run
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# @param style [String]
|
21
|
+
def initialize(style)
|
22
|
+
@style = style
|
23
|
+
end
|
24
|
+
|
25
|
+
# @return [void]
|
26
|
+
def run
|
27
|
+
return dom_tag unless dom_available?
|
28
|
+
|
29
|
+
insert_style_in_head
|
30
|
+
nil
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
# @return [Boolean]
|
36
|
+
def dom_available?
|
37
|
+
false
|
38
|
+
# check for header in DOM rendrer
|
39
|
+
end
|
40
|
+
|
41
|
+
# @return [void]
|
42
|
+
def insert_style_in_head
|
43
|
+
false
|
44
|
+
end
|
45
|
+
|
46
|
+
# @return [String]
|
47
|
+
def dom_tag
|
48
|
+
<<~HTML
|
49
|
+
<style type='text/css'>#{@style}</style>
|
50
|
+
HTML
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ::AmberComponent
|
4
|
+
# Contains the content and type of an asset.
|
5
|
+
class TypedContent
|
6
|
+
class << self
|
7
|
+
# @param val [Hash, self]
|
8
|
+
# @return [self]
|
9
|
+
def wrap(val)
|
10
|
+
return val if val.is_a?(self)
|
11
|
+
|
12
|
+
unless val.respond_to?(:[])
|
13
|
+
raise InvalidType, "`TypedContent` should be a `Hash` or `#{self}` but was `#{val.class}` (#{val.inspect})"
|
14
|
+
end
|
15
|
+
|
16
|
+
new(type: val[:type], content: val[:content])
|
17
|
+
end
|
18
|
+
|
19
|
+
alias [] wrap
|
20
|
+
end
|
21
|
+
|
22
|
+
# @param type [Symbol, String, nil]
|
23
|
+
# @param content [String, Proc, nil]
|
24
|
+
def initialize(type:, content:)
|
25
|
+
@type = type&.to_sym
|
26
|
+
@content = content
|
27
|
+
freeze
|
28
|
+
end
|
29
|
+
|
30
|
+
# @return [Symbol, nil]
|
31
|
+
attr_reader :type
|
32
|
+
# @return [String, Proc, nil]
|
33
|
+
attr_reader :content
|
34
|
+
|
35
|
+
# Stringified content.
|
36
|
+
#
|
37
|
+
# @return [String]
|
38
|
+
def to_s
|
39
|
+
return @content.call.to_s if @content.is_a?(::Proc)
|
40
|
+
|
41
|
+
@content.to_s
|
42
|
+
end
|
43
|
+
|
44
|
+
alias string_content to_s
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails'
|
4
|
+
require 'active_support'
|
5
|
+
require 'active_support/core_ext'
|
6
|
+
|
7
|
+
module ::AmberComponent
|
8
|
+
class Error < ::StandardError; end
|
9
|
+
class ViewFileNotFound < Error; end
|
10
|
+
class InvalidType < Error; end
|
11
|
+
|
12
|
+
class EmptyView < Error; end
|
13
|
+
class UnknownViewType < Error; end
|
14
|
+
class MultipleViews < Error; end
|
15
|
+
|
16
|
+
class EmptyStyle < Error; end
|
17
|
+
class UnknownStyleType < Error; end
|
18
|
+
class MultipleStyles < Error; end
|
19
|
+
end
|
20
|
+
|
21
|
+
require_relative 'amber_component/version'
|
22
|
+
require_relative 'amber_component/helper'
|
23
|
+
require_relative 'amber_component/typed_content'
|
24
|
+
require_relative 'amber_component/base'
|