amber_component 1.1.0 → 1.2.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3fe50a4aaf1482572196e8ae5fd9fd7e2f04ef1ee07b9b5275b0454623e54a6d
4
- data.tar.gz: 17c1e2848263efac38a029621d26a8d2eae5b4b3d47d2c9eaf8e040b14fe3eae
3
+ metadata.gz: '0142826c9117720944d7c6f3ab6a8c56e551ed684c92e1d7ce4857ccbe2632c0'
4
+ data.tar.gz: 749a03720a2a3a6b7131ffc0e432af7dce37e11d4375052f6b051b799f9f66be
5
5
  SHA512:
6
- metadata.gz: e566cb7199d6d8c890081557ed20ac02410085992b036b6a603e909a62ac6fb6af0e853910ab02ac087fd8e3bcaf398d59496e7172ac1233d843206785b15509
7
- data.tar.gz: 3b8bb6257ba09f3f6fbf98c68a128ab99adfa289e8ee3d674de744052b4b69f7b06c0e5f43a5e7bb4ae54533a78728fea15ba2534b947b4e730869b5e09b8932
6
+ metadata.gz: c06aa0648eb8767949de4b69ab288a1534333f0281095d757daf9564402c24c896bb94b78206d2b9219ddbccd61db1dc67281480ee8ac52abf541d8c17e18bc9
7
+ data.tar.gz: 196d879455fc376f6558048e801522a865753ece6aec2ecb6f0888a867707a43a04b629d87c2a2767800280482ec9d375cf56008e74bdbafd56d4b5ab5f8f2e5
data/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  ## [Unreleased]
2
2
 
3
- ## [0.1.0] - 2022-07-21
3
+ ## [1.2.0] - 2023-01-25
4
+
5
+ ### Breaking changes
6
+
7
+ - Inline view syntax has been changed from `view :[view_type] { '[content]' }` to `view '[content]', type: :[view_type]`
8
+
9
+ - Overriding the view template when rendering a component has been removed (eg. `ExampleComponent.call view: '<h1>Some overriden view</h1>'`)
10
+
11
+ ### Added
12
+
13
+ - ERB compiled template caching in production
14
+
15
+ ### Changed
16
+
17
+ - View rendering pipeline has been rewritten
18
+
19
+ ### Fixed
20
+
21
+ - Nesting components has been fixed
22
+
23
+ ## [1.1.1] - 2022-11-14
24
+
25
+ ### Added
26
+
27
+ - Support for webpacker
28
+
29
+ ## [1.1.0] - 2022-11-13
30
+
31
+ ### Added
32
+
33
+ - StimulusJS controllers for components
34
+
35
+ ## [1.0.0] - 2022-11-07
4
36
 
5
37
  - Initial release
data/Gemfile CHANGED
@@ -10,7 +10,7 @@ gem 'rake', '~> 13.0'
10
10
  # Development dependencies
11
11
  gem 'byebug'
12
12
  gem 'git'
13
- gem 'haml'
13
+ gem 'haml-rails'
14
14
  gem 'rubocop', '~> 1.21'
15
15
  gem 'sassc'
16
16
  gem 'solargraph'
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- amber_component (1.1.0)
4
+ amber_component (1.2.0)
5
5
  actionview (>= 6)
6
6
  activemodel (>= 6)
7
7
  activesupport (>= 6)
@@ -48,9 +48,15 @@ GEM
48
48
  ffi (1.15.5)
49
49
  git (1.11.0)
50
50
  rchardet (~> 1.8)
51
- haml (5.2.2)
52
- temple (>= 0.8.0)
51
+ haml (6.1.1)
52
+ temple (>= 0.8.2)
53
+ thor
53
54
  tilt
55
+ haml-rails (2.1.0)
56
+ actionpack (>= 5.1)
57
+ activesupport (>= 5.1)
58
+ haml (>= 4.0.6)
59
+ railties (>= 5.1)
54
60
  i18n (1.12.0)
55
61
  concurrent-ruby (~> 1.0)
56
62
  jaro_winkler (1.5.4)
@@ -139,7 +145,7 @@ GEM
139
145
  thor (~> 1.0)
140
146
  tilt (~> 2.0)
141
147
  yard (~> 0.9, >= 0.9.24)
142
- temple (0.8.2)
148
+ temple (0.10.0)
143
149
  thor (1.2.1)
144
150
  tilt (2.0.11)
145
151
  tzinfo (2.0.5)
@@ -159,7 +165,7 @@ DEPENDENCIES
159
165
  bundler-audit
160
166
  byebug
161
167
  git
162
- haml
168
+ haml-rails
163
169
  minitest (~> 5.0)
164
170
  railties
165
171
  rake (~> 13.0)
data/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![Maintainability](https://api.codeclimate.com/v1/badges/ad84af499e9791933a87/maintainability)](https://codeclimate.com/github/amber-ruby/amber_component/maintainability)
4
4
  [![CI badge](https://github.com/amber-ruby/amber_component/actions/workflows/ci_ruby.yml/badge.svg)](https://github.com/amber-ruby/amber_component/actions/workflows/ci_ruby.yml)
5
5
  [![Coverage Badge](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/Verseth/6a095c79278b074d79feaa4f8ceeb2a8/raw/amber_component__heads_main.json)](https://github.com/amber-ruby/amber_component/actions/workflows/ci_ruby.yml)
6
- [![Downloads](https://ruby-gem-downloads-badge.herokuapp.com/amber_component)]((https://rubygems.org/gems/amber_component))
6
+ <!-- [![Downloads](https://ruby-gem-downloads-badge.herokuapp.com/amber_component)]((https://rubygems.org/gems/amber_component)) -->
7
7
 
8
8
  <img src="banner.png" width="500px" style="margin-bottom: 2rem;"/>
9
9
 
@@ -54,6 +54,13 @@ Every component consists of:
54
54
  - a style file (css, scss, sass etc.)
55
55
  - [optional] a JavaScript file with a Stimulus controller (if you installed the gem with `--stimulus`)
56
56
 
57
+ `amber_component` automatically detects what kind of view and stylesheet formats your app is configured to use.
58
+
59
+ So if you've got `haml-rails`, components will be generated with `haml`. When your app uses `slim-rails`, components will be generated with `slim`. When your `Gemfile` contains `sassc-rails`, components will use `scss` etc.
60
+
61
+ All of these formats can be overridden in
62
+ an initializer or by adding arguments to the component generator.
63
+
57
64
  ```
58
65
  app/components/
59
66
  ├─ [name]_component.rb
@@ -197,13 +204,20 @@ This will generate a new component in `app/components/[name]_component.rb` along
197
204
  app/components/
198
205
  ├─ [name]_component.rb
199
206
  └─ [name]_component/
200
- ├─ style.css
201
- ├─ view.html.erb
207
+ ├─ style.css # may be `.scss` or `.sass`
208
+ ├─ view.html.erb # may be `.haml` or `.slim`
202
209
  └─ controller.js # if stimulus is configured
203
210
  test/components/
204
211
  └─ [name]_component_test.rb
205
212
  ```
206
213
 
214
+ View and stylesheet formats can be overridden by providing options.
215
+
216
+ ```
217
+ -v, [--view=VIEW] # Indicate what type of view should be generated eg. [:erb, :haml, :slim]
218
+ --styles, -c, [--css=CSS] # Indicate what type of styles should be generated eg. [:css, :scss, :sass]
219
+ ```
220
+
207
221
  ### Component properties
208
222
 
209
223
  There is a neat prop DSL.
@@ -323,7 +337,10 @@ custom HTML to a component.
323
337
 
324
338
  This works similarly to React's `props.children`.
325
339
 
326
- To render the passed nested content call `yield.html_safe` somewhere inside the template/view.
340
+ To render the passed nested content call `children(&block)` somewhere inside the ERB template/view.
341
+ If you're using another template language like Haml,
342
+ you may need to use `children{yield}` instead. This difference
343
+ is due to how these templates are compiled.
327
344
 
328
345
  ```ruby
329
346
  # app/components/modal_component.rb
@@ -344,7 +361,7 @@ end
344
361
 
345
362
  <div class="modal_body">
346
363
  <!-- nested content will be rendered here -->
347
- <%= yield.html_safe %>
364
+ <%= children(&block) %>
348
365
  </div>
349
366
 
350
367
  <div class="modal_footer">
@@ -375,7 +392,7 @@ Note that this will raise an error when no block/nested content is provided.
375
392
 
376
393
  In order to render nested content
377
394
  only when it is present (will work without nested content)
378
- you can use `yield.html_safe if block_given?`
395
+ you can use `children(&block) if block_given?` in ERB templates (or `children{yield} if block_given?` for Haml and others)
379
396
 
380
397
  In general `block_given?` will return `true` when a block/nested content is present, otherwise `false`.
381
398
  You can use it to render content conditionally based on
@@ -411,6 +428,21 @@ This makes component views very flexible and convenient.
411
428
  <% end %>
412
429
  ```
413
430
 
431
+ ### Configuration
432
+
433
+ This gem can be configured in an initializer.
434
+ If you used the installer generator it should already be present.
435
+
436
+ ```ruby
437
+ # config/initializers/amber_component.rb
438
+
439
+ ::AmberComponent.configure do |c|
440
+ c.stimulus = nil # [nil, :importmap, :webpacker, :jsbundling, :webpack, :esbuild, :rollup]
441
+ c.stylesheet_format = :css # [:css, :scss, :sass]
442
+ c.view_format = :erb # [:erb, :haml, :slim]
443
+ end
444
+ ```
445
+
414
446
  ### Testing Components
415
447
 
416
448
  ### Rails
@@ -5,15 +5,15 @@ module ::AmberComponent
5
5
  module Assets
6
6
  # Class methods for assets.
7
7
  module ClassMethods
8
- # @return [String]
8
+ # @return [Pathname]
9
9
  def asset_dir_path
10
- component_file_path, = source_location
10
+ component_file_path = source_location.first
11
11
  return asset_dir_from_name unless component_file_path
12
12
 
13
- component_file_path.delete_suffix('.rb')
13
+ ::Pathname.new component_file_path.delete_suffix('.rb')
14
14
  end
15
15
 
16
- # @return [String, nil]
16
+ # @return [Pathname, nil]
17
17
  def asset_dir_from_name
18
18
  return unless defined?(::Rails)
19
19
 
@@ -23,17 +23,17 @@ module ::AmberComponent
23
23
  # Get an array of all folders containing component assets.
24
24
  # This method should only be used on the parent class `AmberComponent::Base` or `ApplicationComponent`.
25
25
  #
26
- # @return [Array<String>]
26
+ # @return [Array<Pathname>]
27
27
  def all_asset_dir_paths
28
28
  subclasses.map(&:asset_dir_path)
29
29
  end
30
30
 
31
- # @param file_name [String, nil]
32
- # @return [String, nil]
31
+ # @param file_name [String, Pathname, nil]
32
+ # @return [Pathname, nil]
33
33
  def asset_path(file_name)
34
34
  return unless file_name
35
35
 
36
- ::File.join(asset_dir_path, file_name)
36
+ asset_dir_path / file_name
37
37
  end
38
38
 
39
39
  # Returns the name of the file inside the asset directory
@@ -45,15 +45,12 @@ module ::AmberComponent
45
45
  return [] unless ::File.directory?(asset_dir_path)
46
46
 
47
47
  ::Dir.entries(asset_dir_path).select do |file|
48
- next unless ::File.file?(::File.join(asset_dir_path, file))
48
+ next unless ::File.file?(asset_dir_path / file)
49
49
 
50
50
  file.match? type_regexp
51
51
  end
52
52
  end
53
53
  end
54
54
 
55
- # Instance methods for assets.
56
- module InstanceMethods
57
- end
58
55
  end
59
56
  end
@@ -42,9 +42,7 @@ module ::AmberComponent
42
42
  extend Helpers::ClassHelper
43
43
 
44
44
  include Helpers::CssHelper
45
- include Views::InstanceMethods
46
45
  extend Views::ClassMethods
47
- include Assets::InstanceMethods
48
46
  extend Assets::ClassMethods
49
47
  include Rendering::InstanceMethods
50
48
  extend Rendering::ClassMethods
@@ -57,18 +55,40 @@ module ::AmberComponent
57
55
  memoize :asset_dir_path
58
56
 
59
57
  # Memoize these methods in production
60
- if ::ENV['RAILS_ENV'] == 'production'
58
+ if defined?(::Rails.env) && ::Rails.env.production?
61
59
  memoize :view_path
62
60
  memoize :view_file_name
63
61
  memoize :view_type
64
62
  end
65
63
 
64
+ # @return [Class]
65
+ def compiled_method_container
66
+ self
67
+ end
68
+
69
+ # @return [String]
70
+ def type
71
+ 'text/html'
72
+ end
73
+
74
+ # @return [Symbol]
75
+ def format
76
+ :html
77
+ end
78
+
79
+ # @return [String]
80
+ def identifier
81
+ source_location.first
82
+ end
83
+
66
84
  private
67
85
 
68
86
  # @param subclass [Class]
69
87
  # @return [void]
70
88
  def inherited(subclass)
71
89
  super
90
+ return unless subclass.name
91
+
72
92
  method_body = lambda do |**kwargs, &block|
73
93
  subclass.render(**kwargs, &block)
74
94
  end
@@ -130,6 +150,11 @@ module ::AmberComponent
130
150
  end
131
151
  end
132
152
 
153
+ # @return [Class]
154
+ def compiled_method_container
155
+ self.class
156
+ end
157
+
133
158
  private
134
159
 
135
160
  # @param kwargs [Hash{Symbol => Object}]
@@ -4,18 +4,36 @@ module ::AmberComponent
4
4
  # Object which stores configuration options
5
5
  # for this gem.
6
6
  class Configuration
7
- # @return [Array<Symbol>]
8
- STIMULUS_INTEGRATIONS = %i[importmap jsbundling webpack esbuild rollup].freeze
7
+ # @return [Set<Symbol>]
8
+ STIMULUS_INTEGRATIONS = ::Set[nil, :importmap, :webpacker, :jsbundling, :webpack, :esbuild, :rollup]
9
+ # @return [Set<Symbol>]
10
+ ALLOWED_STYLES = ::Set.new(%i[css scss sass])
11
+ # @return [Set<Symbol>]
12
+ ALLOWED_VIEWS = ::Set.new(%i[erb haml slim])
9
13
 
10
14
  # How Stimulus.js is bundled in this app.
11
- # Possible values: `[nil, :importmap, :jsbundling, :webpack, :esbuild, :rollup]`
15
+ # Possible values: `[nil, :importmap, :webpacker, :jsbundling, :webpack, :esbuild, :rollup]`
12
16
  # `nil` indicates that stimulus should not be used (default behaviour).
13
17
  #
14
18
  # @return [Symbol, nil]
15
19
  attr_reader :stimulus
16
20
 
21
+ # The default format that the generators will use
22
+ # for the view/template file of a component.
23
+ # Possible values: `[nil, :erb, :haml, :slim]`
24
+ #
25
+ # @return [Symbol, nil]
26
+ attr_reader :view_format
27
+
28
+ # The default format that the generators will use
29
+ # for the stylesheets of a component.
30
+ # Possible values: `[nil, :css, :scss, :sass]`
31
+ #
32
+ # @return [Symbol, nil]
33
+ attr_reader :stylesheet_format
34
+
17
35
  # How Stimulus.js is bundled in this app.
18
- # Possible values: `[nil, :importmap, :jsbundling, :webpack, :esbuild, :rollup]`
36
+ # Possible values: `[nil, :importmap, :webpacker, :jsbundling, :webpack, :esbuild, :rollup]`
19
37
  # `nil` indicates that stimulus should not be used (default behaviour).
20
38
  #
21
39
  # @param val [Symbol, String, nil]
@@ -23,13 +41,37 @@ module ::AmberComponent
23
41
  val = val&.to_sym
24
42
  unless val.nil? || STIMULUS_INTEGRATIONS.include?(val)
25
43
  raise(::ArgumentError,
26
- "Invalid value for `stimulus` bundling. " \
44
+ "Invalid value for `#{__method__}` bundling. " \
27
45
  "Received #{val.inspect}, expected one of #{STIMULUS_INTEGRATIONS.inspect}")
28
46
  end
29
47
 
30
48
  @stimulus = val
31
49
  end
32
50
 
51
+ # @param val [Symbol, String, nil]
52
+ def stylesheet_format=(val)
53
+ val = val&.to_sym
54
+ unless val.nil? || ALLOWED_STYLES.include?(val)
55
+ raise(::ArgumentError,
56
+ "Invalid value for `#{__method__}`. " \
57
+ "Received #{val.inspect}, expected one of #{ALLOWED_STYLES.inspect}")
58
+ end
59
+
60
+ @stylesheet_format = val
61
+ end
62
+
63
+ # @param val [Symbol, String, nil]
64
+ def view_format=(val)
65
+ val = val&.to_sym
66
+ unless val.nil? || ALLOWED_VIEWS.include?(val)
67
+ raise(::ArgumentError,
68
+ "Invalid value for `#{__method__}`. " \
69
+ "Received #{val.inspect}, expected one of #{ALLOWED_VIEWS.inspect}")
70
+ end
71
+
72
+ @view_format = val
73
+ end
74
+
33
75
  # @return [Boolean]
34
76
  def stimulus?
35
77
  !@stimulus.nil?
@@ -3,17 +3,82 @@
3
3
  module ::AmberComponent
4
4
  # Provides universal methods for rendering components.
5
5
  module Rendering
6
+
7
+ # @return [Symbol]
8
+ RENDER_TEMPLATE_METHOD_NAME = :__render
9
+
6
10
  # Class methods for rendering.
7
11
  module ClassMethods
8
12
  # @param kwargs [Hash{Symbol => Object}]
9
13
  # @return [String]
10
14
  def render(**kwargs, &block)
11
- comp = new(**kwargs)
12
-
13
- comp.render(&block)
15
+ new(**kwargs).render(&block)
14
16
  end
15
17
 
16
18
  alias call render
19
+
20
+ # @return [Boolean]
21
+ def compiled?
22
+ method_defined?(RENDER_TEMPLATE_METHOD_NAME)
23
+ end
24
+
25
+ # @return [Boolean]
26
+ def compile?
27
+ return true if defined?(::Rails.env) && ::Rails.env.development?
28
+
29
+ !compiled?
30
+ end
31
+
32
+ # @param force [Boolean] force recompilation
33
+ # @return [void]
34
+ def compile(force: false)
35
+ return if !compile? && !force
36
+ return if template_handler.nil?
37
+
38
+ render_template_method_redefinition_lock.synchronize do
39
+ silence_redefinition_of_method(RENDER_TEMPLATE_METHOD_NAME)
40
+ # rubocop:disable Style/EvalWithLocation
41
+ class_eval <<~CODE, view_path.to_s, 0 # rubocop:disable Style/DocumentDynamicEvalDefinition
42
+ def #{RENDER_TEMPLATE_METHOD_NAME}(local_assigns, output_buffer, &block)
43
+ #{compiled_template_source}
44
+ end
45
+ CODE
46
+ # rubocop:enable Style/EvalWithLocation
47
+ end
48
+ end
49
+
50
+ # @return [Class, nil]
51
+ def template_handler
52
+ @template_handler ||= ::ActionView::Template.registered_template_handler(view_type)
53
+ end
54
+
55
+ private
56
+
57
+ # @return [Mutex]
58
+ def render_template_method_redefinition_lock
59
+ @render_template_method_redefinition_lock ||= ::Mutex.new
60
+ end
61
+
62
+ # @return [String]
63
+ def compiled_template_source
64
+ handler = template_handler
65
+ unless handler
66
+ raise UnknownViewTypeError,
67
+ "view type `#{view_type.inspect}` is not known in #{self}, " \
68
+ "available types: #{::ActionView::Template.template_handler_extensions.inspect}"
69
+ end
70
+
71
+ if handler.method(:call).parameters.length > 1
72
+ handler.call(self, view_template_source)
73
+ else
74
+ handler.call(
75
+ source: view_template_source,
76
+ identifier: identifier,
77
+ type: type
78
+ )
79
+ end
80
+ end
81
+
17
82
  end
18
83
 
19
84
  # Instance methods for rendering.
@@ -21,10 +86,7 @@ module ::AmberComponent
21
86
  # @return [String]
22
87
  def render(&block)
23
88
  run_callbacks :render do
24
- element = render_view(&block)
25
- # styles = inject_styles
26
- # element += styles unless styles.nil?
27
- element.html_safe
89
+ compile_and_render(&block)
28
90
  end
29
91
  end
30
92
 
@@ -39,13 +101,49 @@ module ::AmberComponent
39
101
  render
40
102
  end
41
103
 
42
- protected
104
+ # @param args [Array<Object>]
105
+ # @return [String]
106
+ def nested_content(*args, &block)
107
+ block_self = block.binding.receiver
108
+ return block_self.safe_capture(*args, &block) if block_self.respond_to?(:safe_capture)
109
+
110
+ safe_capture(*args, &block)
111
+ end
112
+ alias children nested_content
113
+
114
+ def safe_capture(*args)
115
+ value = nil
116
+ buffer = with_output_buffer { value = yield(*args) }
117
+ buffer.presence || value.html_safe
118
+ end
119
+
120
+ private
121
+
122
+ # @return [String]
123
+ def compile_and_render(&block)
124
+ self.class.compile
125
+ if self.class.compiled?
126
+ return _run(
127
+ RENDER_TEMPLATE_METHOD_NAME,
128
+ self.class.template_handler,
129
+ [],
130
+ ::ActionView::OutputBuffer.new,
131
+ &block
132
+ )
133
+ end
134
+
135
+ render_non_rails_string(
136
+ self.class.view_template_source,
137
+ self.class.view_type,
138
+ block
139
+ )
140
+ end
43
141
 
44
142
  # @param content [String]
45
143
  # @param type [Symbol]
46
144
  # @param block [Proc, nil]
47
145
  # @return [String]
48
- def render_string(content, type, block = nil)
146
+ def render_non_rails_string(content, type, block = nil)
49
147
  TemplateHandler.render_from_string(self, content, type, block)
50
148
  end
51
149
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module ::AmberComponent
4
4
  # Provides code which handles rendering different
5
- # template languages.
5
+ # template languages outside of Rails.
6
6
  module TemplateHandler
7
7
  class << self
8
8
  # @param context [AmberComponent::Base]
@@ -11,16 +11,15 @@ module ::AmberComponent
11
11
  # @param block [Proc, nil]
12
12
  # @return [String]
13
13
  def render_from_string(context, content, type, block = nil)
14
- options = if type.to_sym == :erb
15
- { engine_class: ERB }
16
- else
17
- {}
18
- end
14
+ tilt_handler = ::Tilt[type]
15
+ raise UnknownViewTypeError, <<~ERR.squish unless tilt_handler
16
+ Unknown view type for `#{context.class}`!
17
+ Check return value of param type in `view type: :[type]`
18
+ or the view file extension.
19
+ ERR
19
20
 
20
- ::Tilt[type].new(options) { content }.render(context, &block)
21
+ tilt_handler.new { content }.render(context, &block).html_safe
21
22
  end
22
23
  end
23
24
  end
24
25
  end
25
-
26
- require_relative 'template_handler/erb'
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ::AmberComponent
4
- VERSION = '1.1.0'
4
+ VERSION = '1.2.0'
5
5
  end
@@ -1,14 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'pathname'
4
+
3
5
  module ::AmberComponent
4
6
  # Provides methods concerning view registering and rendering.
5
7
  module Views
6
- # View types with built-in embedded Ruby
7
- #
8
- # @return [Set<Symbol>]
9
- VIEW_TYPES_WITH_RUBY = ::Set[:erb, :haml, :slim].freeze
10
- # @return [Set<Symbol>]
11
- ALLOWED_VIEW_TYPES = ::Set[:erb, :haml, :slim, :html, :md, :markdown].freeze
12
8
  # @return [Regexp]
13
9
  VIEW_FILE_REGEXP = /^view\./.freeze
14
10
 
@@ -18,28 +14,25 @@ module ::AmberComponent
18
14
  #
19
15
  # Usage:
20
16
  #
21
- # view do
22
- # <<~ERB
23
- # <h1>
24
- # Hello <%= @name %>
25
- # </h1>
26
- # ERB
27
- # end
17
+ # view <<~ERB
18
+ # <h1>
19
+ # Hello <%= @name %>
20
+ # </h1>
21
+ # ERB
28
22
  #
29
23
  # or:
30
24
  #
31
- # view :haml do
32
- # <<~HAML
33
- # %h1
34
- # Hello
35
- # = @name
36
- # HAML
37
- # end
25
+ # view <<~HAML, type: :haml
26
+ # %h1
27
+ # Hello
28
+ # = @name
29
+ # HAML
38
30
  #
31
+ # @param content [String, Proc]
39
32
  # @param type [Symbol]
40
33
  # @return [void]
41
- def view(type = :erb, &block)
42
- @method_view = TypedContent.new(type: type, content: block)
34
+ def view(content, type: :erb)
35
+ @method_view = TypedContent.new(type: type, content: content)
43
36
  end
44
37
 
45
38
  # ERB/Haml/Slim view registered through the `view` method.
@@ -48,6 +41,13 @@ module ::AmberComponent
48
41
  attr_reader :method_view
49
42
 
50
43
  # @return [String]
44
+ def view_template_source
45
+ return @method_view.to_s if @method_view
46
+
47
+ ::File.read(view_path)
48
+ end
49
+
50
+ # @return [String, nil]
51
51
  def view_path
52
52
  asset_path view_file_name
53
53
  end
@@ -62,136 +62,11 @@ module ::AmberComponent
62
62
 
63
63
  # @return [Symbol]
64
64
  def view_type
65
- (view_file_name.split('.')[1..].grep_v(/erb/).last || 'erb')&.to_sym
66
- end
67
- end
68
-
69
- # Instance methods for views.
70
- module InstanceMethods
71
- protected
72
-
73
- # @return [String]
74
- def render_view(&block)
75
- view_from_file = render_view_from_file(&block)
76
- view_from_method = render_class_method_view(&block)
77
- view_from_inline = render_view_from_inline(&block)
78
-
79
- view_content = view_from_file unless view_from_file.empty?
80
- view_content = view_from_method unless view_from_method.empty?
81
- view_content = view_from_inline unless view_from_inline.empty?
82
-
83
- if view_content.nil? || view_content.empty?
84
- raise ViewFileNotFoundError, "View for `#{self.class}` could not be found!"
85
- end
86
-
87
- view_content
88
- end
89
-
90
- # Helper method to render view from string or with other provided type.
91
- #
92
- # Usage:
93
- #
94
- # render_view_from_content('<h1>Hello World</h1>')
95
- #
96
- # or:
97
- #
98
- # render_view_from_content content: '**Hello World**', type: 'md'
99
- #
100
- # @param content [TypedContent, Hash{Symbol => String, Symbol, Proc}, String]
101
- # @return [String, nil]
102
- def render_view_from_content(content, &block)
103
- return '' unless content
104
- return content if content.is_a?(::String)
105
-
106
- content = TypedContent.wrap(content)
107
- type = content.type
108
- content = content.to_s
109
-
110
- if content.empty?
111
- raise EmptyViewError, <<~ERR.squish
112
- Custom view for `#{self.class}` from view method cannot be empty!
113
- ERR
114
- end
115
-
116
- unless ALLOWED_VIEW_TYPES.include? type
117
- raise UnknownViewTypeError, <<~ERR.squish
118
- Unknown view type for `#{self.class}` from view method!
119
- Check return value of param type in `view :[type] do`
120
- ERR
121
- end
122
-
123
- unless VIEW_TYPES_WITH_RUBY.include? type
124
- # first render the content with ERB if the
125
- # type does not support embedding Ruby by default
126
- content = render_string(content, :erb, block)
127
- end
128
-
129
- render_string(content, type, block)
130
- end
131
-
132
- # @return [String]
133
- def render_view_from_file(&block)
134
- view_path = self.class.view_path
135
- return '' if view_path.nil? || !::File.file?(view_path)
136
-
137
- content = ::File.read(view_path)
138
- type = self.class.view_type
139
-
140
- unless VIEW_TYPES_WITH_RUBY.include? type
141
- content = render_string(content, :erb, block)
142
- end
143
-
144
- render_string(content, type, block)
145
- end
146
-
147
- # Method returning view from method in class file.
148
- # Usage:
149
- #
150
- # view do
151
- # <<~HTML
152
- # <h1>
153
- # Hello <%= @name %>
154
- # </h1>
155
- # HTML
156
- # end
157
- #
158
- # or:
159
- #
160
- # view :haml do
161
- # <<~HAML
162
- # %h1
163
- # Hello
164
- # = @name
165
- # HAML
166
- # end
167
- #
168
- # @return [String]
169
- def render_class_method_view(&block)
170
- render_view_from_content(self.class.method_view, &block)
171
- end
172
-
173
- # Method returning view from params in view.
174
- # Usage:
175
- #
176
- # <%= ExampleComponent data: data, view: "<h1>Hello #{@name}</h1>" %>
177
- #
178
- # or:
179
- #
180
- # <%= ExampleComponent data: data, view: { content: "<h1>Hello #{@name}</h1>", type: 'erb' } %>
181
- #
182
- # @return [String]
183
- def render_view_from_inline(&block)
184
- data = \
185
- if @view.is_a? ::String
186
- TypedContent.new(
187
- type: :erb,
188
- content: @view
189
- )
190
- else
191
- @view
192
- end
65
+ return @method_view.type if @method_view
66
+ raise ViewFileNotFoundError, "No view file for #{self}" unless view_file_name
193
67
 
194
- render_view_from_content(data, &block)
68
+ view_file_path = ::Pathname.new view_file_name
69
+ view_file_path.extname.delete_prefix('.').to_sym
195
70
  end
196
71
  end
197
72
  end
@@ -3,6 +3,7 @@
3
3
  require 'active_support'
4
4
  require 'active_support/core_ext'
5
5
  require 'pathname'
6
+ require 'set'
6
7
 
7
8
  require_relative 'amber_component/configuration'
8
9
 
@@ -11,7 +12,6 @@ module ::AmberComponent
11
12
  class Error < ::StandardError; end
12
13
  class MissingPropsError < Error; end
13
14
  class IncorrectPropTypeError < Error; end
14
- class ViewFileNotFoundError < Error; end
15
15
  class InvalidTypeError < Error; end
16
16
 
17
17
  class EmptyViewError < Error; end
@@ -10,14 +10,22 @@ module ::AmberComponent
10
10
  desc 'Install the AmberComponent gem'
11
11
  source_root ::File.expand_path('templates', __dir__)
12
12
 
13
- # @return [Array<Symbol>]
14
- STIMULUS_INTEGRATIONS = %i[stimulus importmap jsbundling webpack esbuild rollup].freeze
15
-
16
13
  class_option :stimulus,
17
14
  desc: "Configure the app to use Stimulus.js wih components to make them interactive " \
18
- "[options: importmap (default), jsbundling, webpack, esbuild, rollup]"
15
+ "[options: importmap (default), webpacker (legacy), jsbundling, webpack, esbuild, rollup]"
16
+
17
+ class_option :styles,
18
+ desc: "Configure the app to generate components with a particular stylesheet format " \
19
+ "[options: css (default), scss, sass]"
20
+
21
+ class_option :views,
22
+ desc: "Configure the app to generate components with a particular view format " \
23
+ "[options: erb (default), haml, slim]"
19
24
 
20
25
  def setup
26
+ detect_stimulus
27
+ detect_styles
28
+ detect_views
21
29
  copy_file 'application_component.rb', 'app/components/application_component.rb'
22
30
  copy_file 'application_component_test_case.rb', 'test/application_component_test_case.rb'
23
31
  append_file 'test/test_helper.rb', "require_relative 'application_component_test_case'"
@@ -30,40 +38,107 @@ module ::AmberComponent
30
38
  require_components_css_in 'app/assets/stylesheets/application.scss.sass'
31
39
  require_components_css_in 'app/assets/stylesheets/application.sass.scss'
32
40
  configure_stimulus
41
+ create_initializer
33
42
  end
34
43
 
35
44
  private
36
45
 
37
- def configure_stimulus
38
- stimulus = options[:stimulus]&.to_sym
39
- return unless stimulus
46
+ def detect_styles
47
+ styles_option = options[:styles]&.to_sym
48
+ if !styles_option.nil? && !Configuration::ALLOWED_STYLES.include?(styles_option)
49
+ raise ::ArgumentError, "no such `stylesheet_format` as #{styles_option.inspect}"
50
+ end
40
51
 
41
- case stimulus
52
+ @styles =
53
+ if styles_option
54
+ styles_option
55
+ elsif defined?(::SassC)
56
+ :scss
57
+ else
58
+ :css
59
+ end
60
+ end
61
+
62
+ def detect_views
63
+ views_option = options[:views]&.to_sym
64
+ if !views_option.nil? && !Configuration::ALLOWED_VIEWS.include?(views_option)
65
+ raise ::ArgumentError, "no such `view_format` as #{views_option.inspect}"
66
+ end
67
+
68
+ @views =
69
+ if views_option
70
+ views_option
71
+ elsif defined?(::Haml)
72
+ :haml
73
+ elsif defined?(::Slim)
74
+ :slim
75
+ else
76
+ :erb
77
+ end
78
+ end
79
+
80
+ def detect_stimulus
81
+ stimulus_option = options[:stimulus]&.to_sym
82
+ return unless stimulus_option
83
+
84
+ case stimulus_option
42
85
  when :stimulus
43
86
  if defined?(::Jsbundling)
44
- stimulus_integration = :jsbundling
45
- configure_stimulus_jsbundling
87
+ stimulus_jsbundling!
88
+ elsif defined?(::Webpacker)
89
+ stimulus_webpacker!
46
90
  else
47
- stimulus_integration = :importmap
48
- configure_stimulus_importmap
91
+ stimulus_importmap!
49
92
  end
50
93
  when :importmap
51
- stimulus_integration = :importmap
52
- configure_stimulus_importmap
94
+ stimulus_importmap!
53
95
  when :jsbundling, :webpack, :esbuild, :rollup
54
- stimulus_integration = :jsbundling
55
- configure_stimulus_jsbundling
96
+ stimulus_jsbundling!
97
+ when :webpacker
98
+ stimulus_webpacker!
99
+ else
100
+ raise ::ArgumentError,
101
+ "no such stimulus integration as `#{options[:stimulus].inspect}`"
56
102
  end
103
+ end
104
+
105
+ def assert_styles
106
+ return if options[:styles].nil?
107
+ return if options[:styles].nil?
108
+ end
57
109
 
110
+ def configure_stimulus
111
+ case @stimulus
112
+ when :importmap then configure_stimulus_importmap
113
+ when :jsbundling then configure_stimulus_jsbundling
114
+ when :webpacker then configure_stimulus_webpacker
115
+ end
116
+ end
117
+
118
+ def create_initializer
58
119
  create_file 'config/initializers/amber_component.rb', <<~RUBY
59
120
  # frozen_string_literal: true
60
121
 
61
122
  ::AmberComponent.configure do |c|
62
- c.stimulus = :#{stimulus_integration}
123
+ c.stimulus = #{@stimulus.inspect} # #{Configuration::STIMULUS_INTEGRATIONS.to_a}
124
+ c.stylesheet_format = #{@styles.inspect} # #{Configuration::ALLOWED_STYLES.to_a}
125
+ c.view_format = #{@views.inspect} # #{Configuration::ALLOWED_VIEWS.to_a}
63
126
  end
64
127
  RUBY
65
128
  end
66
129
 
130
+ def stimulus_jsbundling!
131
+ @stimulus = :jsbundling
132
+ end
133
+
134
+ def stimulus_importmap!
135
+ @stimulus = :importmap
136
+ end
137
+
138
+ def stimulus_webpacker!
139
+ @stimulus = :webpacker
140
+ end
141
+
67
142
  def configure_stimulus_importmap
68
143
  install_importmap
69
144
  install_stimulus
@@ -88,6 +163,17 @@ module ::AmberComponent
88
163
  JS
89
164
  end
90
165
 
166
+ def configure_stimulus_webpacker
167
+ install_stimulus
168
+ append_file 'app/javascript/packs/application.js', %(import "controllers"\n)
169
+ append_file 'app/javascript/controllers/index.js', %(import "./components"\n)
170
+ create_file 'app/javascript/controllers/components.js', <<~JS
171
+ // This file has been created by `amber_component` and will
172
+ // register all stimulus controllers from your components
173
+ import { application } from "./application"
174
+ JS
175
+ end
176
+
91
177
  # @return [void]
92
178
  def install_importmap
93
179
  return if ::File.exist?('config/importmap.rb') && defined?(::Importmap)
@@ -7,33 +7,26 @@ class AmberComponentGenerator < ::Rails::Generators::NamedBase
7
7
  desc 'Generate a new component'
8
8
  source_root ::File.expand_path('templates', __dir__)
9
9
 
10
- # @return [Array<Symbol>]
11
- VIEW_FORMATS = %i[html erb haml slim].freeze
12
- # @return [Array<Symbol>]
13
- STYLE_FORMATS = %i[css scss sass].freeze
14
-
15
10
  class_option :view,
16
11
  aliases: ['-v'],
17
- desc: "Indicate what type of view should be generated eg. #{VIEW_FORMATS}"
12
+ desc: "Indicate what type of view should be generated " \
13
+ "eg. #{::AmberComponent::Configuration::ALLOWED_VIEWS}"
18
14
 
19
15
  class_option :css,
20
- aliases: ['--style', '-c'],
21
- desc: "Indicate what type of styles should be generated eg. #{STYLE_FORMATS}"
16
+ aliases: ['--styles', '-c'],
17
+ desc: "Indicate what type of styles should be generated " \
18
+ "eg. #{::AmberComponent::Configuration::ALLOWED_STYLES}"
22
19
 
23
20
  def generate_component
24
- @view_format = (options[:view] || :html).to_sym
25
- @view_format = :html if @view_format == :erb
26
-
27
- @style_format = options[:css]&.to_sym
21
+ set_view_format
22
+ set_stylesheet_format
28
23
 
29
- unless VIEW_FORMATS.include? @view_format
30
- puts "No such view format as `#{@view_format}`"
31
- return
24
+ unless ::AmberComponent::Configuration::ALLOWED_VIEWS.include? @view_format
25
+ raise ::ArgumentError, "No such view format as `#{@view_format}`"
32
26
  end
33
27
 
34
- if !@style_format.nil? && STYLE_FORMATS.include?(@style_format)
35
- puts "No such css/style format as `#{@style_format}`"
36
- return
28
+ unless ::AmberComponent::Configuration::ALLOWED_STYLES.include?(@stylesheet_format)
29
+ raise ::ArgumentError, "No such css/style format as `#{@stylesheet_format}`"
37
30
  end
38
31
 
39
32
  template 'component.rb.erb', "app/components/#{file_path}.rb"
@@ -53,6 +46,14 @@ class AmberComponentGenerator < ::Rails::Generators::NamedBase
53
46
 
54
47
  private
55
48
 
49
+ def set_view_format
50
+ @view_format = options[:view]&.to_sym || ::AmberComponent.configuration.view_format || :erb
51
+ end
52
+
53
+ def set_stylesheet_format
54
+ @stylesheet_format = options[:style]&.to_sym || ::AmberComponent.configuration.stylesheet_format || :css
55
+ end
56
+
56
57
  # @return [Boolean]
57
58
  def stimulus?
58
59
  ::AmberComponent.configuration.stimulus?
@@ -90,9 +91,10 @@ class AmberComponentGenerator < ::Rails::Generators::NamedBase
90
91
 
91
92
  # @return [void]
92
93
  def create_stylesheet
93
- if (@style_format.nil? && defined?(::SassC)) || @style_format == :scss
94
+ case @stylesheet_format
95
+ when :scss
94
96
  template 'style.scss.erb', "app/components/#{file_path}/style.scss"
95
- elsif @style_format == :sass
97
+ when :sass
96
98
  template 'style.sass.erb', "app/components/#{file_path}/style.sass"
97
99
  else
98
100
  template 'style.css.erb', "app/components/#{file_path}/style.css"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: amber_component
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ruby-Amber
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: exe
12
12
  cert_chain: []
13
- date: 2022-11-13 00:00:00.000000000 Z
13
+ date: 2023-01-25 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: actionview
@@ -123,7 +123,6 @@ files:
123
123
  - lib/amber_component/railtie.rb
124
124
  - lib/amber_component/rendering.rb
125
125
  - lib/amber_component/template_handler.rb
126
- - lib/amber_component/template_handler/erb.rb
127
126
  - lib/amber_component/test_helper.rb
128
127
  - lib/amber_component/typed_content.rb
129
128
  - lib/amber_component/version.rb
@@ -1,17 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'action_view'
4
- require 'ostruct'
5
-
6
- module ::AmberComponent
7
- module TemplateHandler
8
- # Handles rendering ERB with Rails-like syntax
9
- class ERB < ::ActionView::Template::Handlers::ERB::Erubi
10
- def initialize(input, properties = {})
11
- properties[:bufvar] ||= "@output_buffer"
12
- properties[:preamble] = "#{properties[:bufvar]}=#{::ActionView::OutputBuffer}.new;"
13
- super
14
- end
15
- end
16
- end
17
- end