amber_component 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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