proscenium 0.19.0.beta4-arm64-darwin → 0.19.0.beta6-arm64-darwin

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/lib/proscenium/builder.rb +9 -13
  4. data/lib/proscenium/ext/proscenium +0 -0
  5. data/lib/proscenium/importer.rb +13 -13
  6. data/lib/proscenium/middleware/base.rb +5 -2
  7. data/lib/proscenium/middleware/engines.rb +5 -9
  8. data/lib/proscenium/middleware/esbuild.rb +13 -8
  9. data/lib/proscenium/middleware.rb +2 -4
  10. data/lib/proscenium/railtie.rb +15 -5
  11. data/lib/proscenium/react_componentable.rb +1 -1
  12. data/lib/proscenium/resolver.rb +3 -8
  13. data/lib/proscenium/side_load.rb +1 -1
  14. data/lib/proscenium/ui/flash/index.css +1 -0
  15. data/lib/proscenium/ui/flash/index.js +73 -0
  16. data/lib/proscenium/ui/flash.rb +15 -0
  17. data/lib/proscenium/ui/form/field_methods.rb +88 -0
  18. data/lib/proscenium/ui/form/fields/base.rb +188 -0
  19. data/lib/proscenium/ui/form/fields/checkbox/index.jsx +48 -0
  20. data/lib/proscenium/ui/form/fields/checkbox/index.module.css +9 -0
  21. data/lib/proscenium/ui/form/fields/checkbox/previews/basic.jsx +8 -0
  22. data/lib/proscenium/ui/form/fields/checkbox.rb +32 -0
  23. data/lib/proscenium/ui/form/fields/date.module.css +27 -0
  24. data/lib/proscenium/ui/form/fields/datetime.rb +15 -0
  25. data/lib/proscenium/ui/form/fields/hidden.rb +9 -0
  26. data/lib/proscenium/ui/form/fields/input/index.jsx +71 -0
  27. data/lib/proscenium/ui/form/fields/input/index.module.css +13 -0
  28. data/lib/proscenium/ui/form/fields/input/previews/basic.jsx +8 -0
  29. data/lib/proscenium/ui/form/fields/input.rb +14 -0
  30. data/lib/proscenium/ui/form/fields/radio_group.rb +173 -0
  31. data/lib/proscenium/ui/form/fields/radio_input/index.jsx +44 -0
  32. data/lib/proscenium/ui/form/fields/radio_input/index.module.css +13 -0
  33. data/lib/proscenium/ui/form/fields/radio_input/previews/basic.jsx +8 -0
  34. data/lib/proscenium/ui/form/fields/radio_input.rb +17 -0
  35. data/lib/proscenium/ui/form/fields/rich_textarea.css +23 -0
  36. data/lib/proscenium/ui/form/fields/rich_textarea.js +6 -0
  37. data/lib/proscenium/ui/form/fields/rich_textarea.rb +18 -0
  38. data/lib/proscenium/ui/form/fields/select.jsx +47 -0
  39. data/lib/proscenium/ui/form/fields/select.module.css +46 -0
  40. data/lib/proscenium/ui/form/fields/select.rb +300 -0
  41. data/lib/proscenium/ui/form/fields/tel.css +297 -0
  42. data/lib/proscenium/ui/form/fields/tel.js +83 -0
  43. data/lib/proscenium/ui/form/fields/tel.rb +54 -0
  44. data/lib/proscenium/ui/form/fields/textarea/index.jsx +50 -0
  45. data/lib/proscenium/ui/form/fields/textarea/index.module.css +13 -0
  46. data/lib/proscenium/ui/form/fields/textarea/previews/basic.jsx +8 -0
  47. data/lib/proscenium/ui/form/fields/textarea.rb +18 -0
  48. data/lib/proscenium/ui/form/translation.rb +71 -0
  49. data/lib/proscenium/ui/form.css +52 -0
  50. data/lib/proscenium/ui/form.rb +213 -0
  51. data/lib/proscenium/ui/props.css +7 -0
  52. data/lib/proscenium/ui/react-manager/index.jsx +1 -1
  53. data/lib/proscenium/ui/test.js +1 -1
  54. data/lib/proscenium/ui/ujs/index.js +1 -1
  55. data/lib/proscenium/ui.rb +3 -0
  56. data/lib/proscenium/utils.rb +33 -0
  57. data/lib/proscenium/version.rb +1 -1
  58. data/lib/proscenium/view_component.rb +0 -2
  59. data/lib/proscenium.rb +12 -2
  60. metadata +61 -10
  61. data/lib/proscenium/middleware/runtime.rb +0 -18
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 30f6dd1ec92c5a7dd53df353c65757e18ec374d0d1eaf407364d9baaed60d2a6
4
- data.tar.gz: 2aeea6b5c6940af2521afd786db4289ee137c2813f6860ed7c8a880f89d3d6c1
3
+ metadata.gz: f3c6d979009796ee291585c2e5c229c6ad894b7861ed0e6771b2376dee0b29be
4
+ data.tar.gz: be6f8c87c387225c306142e09bb517a47e4b31d0a70e4973c3710d1ca6a8c0d2
5
5
  SHA512:
6
- metadata.gz: cdb6490ba1a47a9c620a16bf8e5b9d8396e552121645cdace3778a8b6a9936fe310cd9881d5c2be1f7b5cdfc1047b963d146b1f72e772e5e99c9e2c70cf09f50
7
- data.tar.gz: 7355f6c13d8aea2c56c6eba5e848b436274e56e29621b46a0c15e63c1b5a116cbaae268fda699d94da650d7a346acf58e9c4c7b58f520b3d3b0ad731aa0a4421
6
+ metadata.gz: d12060a40b22e1353cff66cf276c9caf4963153e4b315530a4bc768a72188299bef188b0c0ff9a706ded739fe407a7014a96a42ba00bfe0297492ea2262406ba
7
+ data.tar.gz: b4ef71bae7c0f66dfeb3873b3fe8c3d544a70bad29cd76d591a32aa6e6d2aecad81222a6f61c2e889b79226c2c761aaac3ff50540c17dd8b089af9bb6d6fffed
data/README.md CHANGED
@@ -413,7 +413,7 @@ if (typeof proscenium.env?.UNKNOWN !== "undefined") {
413
413
  Basic support is provided for importing your Rails locale files from `config/locales/*.yml`, exporting them as JSON.
414
414
 
415
415
  ```js
416
- import translations from "@proscenium/i18n";
416
+ import translations from "proscenium/i18n";
417
417
  // translations.en.*
418
418
  ```
419
419
 
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'ffi'
4
- require 'oj'
5
4
 
6
5
  module Proscenium
7
6
  class Builder
@@ -42,7 +41,7 @@ module Proscenium
42
41
  attr_reader :error
43
42
 
44
43
  def initialize(error)
45
- @error = Oj.load(error, mode: :strict).deep_transform_keys(&:underscore)
44
+ @error = JSON.parse(error, strict: true).deep_transform_keys(&:underscore)
46
45
 
47
46
  msg = @error['text']
48
47
  if (location = @error['location'])
@@ -65,8 +64,8 @@ module Proscenium
65
64
  new(root:).build_to_path(path)
66
65
  end
67
66
 
68
- def self.build_to_string(path, root: nil)
69
- new(root:).build_to_string(path)
67
+ def self.build_to_string(path, root: nil, bundle: nil)
68
+ new(root:, bundle:).build_to_string(path)
70
69
  end
71
70
 
72
71
  def self.resolve(path, root: nil)
@@ -78,15 +77,18 @@ module Proscenium
78
77
  Request.reset_config
79
78
  end
80
79
 
81
- def initialize(root: nil)
80
+ def initialize(root: nil, bundle: nil)
81
+ bundle = Proscenium.config.bundle if bundle.nil?
82
+
82
83
  @request_config = FFI::MemoryPointer.from_string({
83
84
  RootPath: (root || Rails.root).to_s,
84
85
  GemPath: gem_root,
85
86
  Environment: ENVIRONMENTS.fetch(Rails.env.to_sym, 2),
86
- Engines: engines,
87
+ Engines: Proscenium.config.engines,
87
88
  EnvVars: env_vars,
88
89
  CodeSplitting: Proscenium.config.code_splitting,
89
- Bundle: Proscenium.config.bundle,
90
+ ExternalNodeModules: Proscenium.config.external_node_modules,
91
+ Bundle: bundle,
90
92
  Debug: Proscenium.config.debug
91
93
  }.to_json)
92
94
  end
@@ -139,12 +141,6 @@ module Proscenium
139
141
  q ? "--cache-query-string #{q}" : nil
140
142
  end
141
143
 
142
- def engines
143
- Proscenium.config.engines.to_h { |e| [e.engine_name, e.root.to_s] }.tap do |x|
144
- x['proscenium/ui'] = Proscenium.ui_path.to_s
145
- end
146
- end
147
-
148
144
  def gem_root
149
145
  Pathname.new(__dir__).join('..', '..').to_s
150
146
  end
Binary file
@@ -67,28 +67,28 @@ module Proscenium
67
67
  sideload_css(filepath, **options) unless options[:css] == false
68
68
  end
69
69
 
70
- def sideload_js(filepath, **options)
71
- return unless Proscenium.config.side_load
72
-
73
- filepath = Rails.root.join(filepath) unless filepath.is_a?(Pathname)
74
- filepath = filepath.sub_ext('')
70
+ def sideload_js(filepath, **)
71
+ _sideload(filepath, JS_EXTENSIONS, **)
72
+ end
75
73
 
76
- JS_EXTENSIONS.find do |x|
77
- if (fp = filepath.sub_ext(x)).exist?
78
- import(Resolver.resolve(fp.to_s), sideloaded: true, **options)
79
- end
80
- end
74
+ def sideload_css(filepath, **)
75
+ _sideload(filepath, CSS_EXTENSIONS, **)
81
76
  end
82
77
 
83
- def sideload_css(filepath, **options)
78
+ private def _sideload(filepath, extensions, **options) # rubocop:disable Style/AccessModifierDeclarations
84
79
  return unless Proscenium.config.side_load
85
80
 
86
81
  filepath = Rails.root.join(filepath) unless filepath.is_a?(Pathname)
87
82
  filepath = filepath.sub_ext('')
88
83
 
89
- CSS_EXTENSIONS.find do |x|
84
+ extensions.find do |x|
90
85
  if (fp = filepath.sub_ext(x)).exist?
91
- import(Resolver.resolve(fp.to_s), sideloaded: true, **options)
86
+ if (fp = fp.to_s).start_with?(Proscenium.ui_path.to_s)
87
+ fp.sub!(Proscenium.ui_path_regex, 'proscenium/')
88
+ import(Resolver.resolve(fp), sideloaded: true, **options)
89
+ else
90
+ import(Resolver.resolve(fp.to_s), sideloaded: true, **options)
91
+ end
92
92
  end
93
93
  end
94
94
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'oj'
4
-
5
3
  module Proscenium
6
4
  class Middleware
7
5
  class Base
@@ -90,6 +88,11 @@ module Proscenium
90
88
  response.cache! Proscenium.config.cache_max_age
91
89
  end
92
90
 
91
+ cache_proc = Proscenium.config.cache_middleware_response
92
+ if cache_proc.is_a?(Proc) && cache_proc.call(path_to_build)
93
+ response.cache! Proscenium.config.cache_max_age
94
+ end
95
+
93
96
  yield response if block_given?
94
97
 
95
98
  response.finish
@@ -10,7 +10,7 @@ module Proscenium
10
10
  #
11
11
  # module Gem1
12
12
  # class Engine < ::Rails::Engine
13
- # config.proscenium.engines << self
13
+ # config.proscenium.engines['gem1'] = root
14
14
  # end
15
15
  # end
16
16
  #
@@ -24,21 +24,17 @@ module Proscenium
24
24
  end
25
25
 
26
26
  def root_for_readable
27
- ui? ? Proscenium.ui_path : engine.root
27
+ engine.last
28
28
  end
29
29
 
30
30
  def engine
31
- @engine ||= Proscenium.config.engines.find do |x|
32
- @request.path.start_with?("/#{x.engine_name}")
31
+ @engine ||= Proscenium.config.engines.find do |k, _|
32
+ @request.path.start_with?("/#{k}")
33
33
  end
34
34
  end
35
35
 
36
36
  def engine_name
37
- ui? ? 'proscenium/ui' : engine.engine_name
38
- end
39
-
40
- def ui?
41
- @request.path.start_with?('/proscenium/ui/')
37
+ engine.first
42
38
  end
43
39
  end
44
40
  end
@@ -6,21 +6,26 @@ module Proscenium
6
6
  class CompileError < Base::CompileError
7
7
  def initialize(args)
8
8
  detail = args[:detail]
9
- detail = ActiveSupport::HashWithIndifferentAccess.new(Oj.load(detail, mode: :strict))
9
+ detail = JSON.parse(detail, mode: :strict)
10
10
 
11
- args[:detail] = if detail[:location]
12
- "#{detail[:text]} in #{detail[:location][:file]}:" +
13
- detail[:location][:line].to_s
14
- else
15
- detail[:text]
16
- end
11
+ args['detail'] = if detail['location']
12
+ "#{detail['text']} in #{detail['location']['file']}:" +
13
+ detail['location']['line'].to_s
14
+ else
15
+ detail['text']
16
+ end
17
17
 
18
18
  super
19
19
  end
20
20
  end
21
21
 
22
22
  def attempt
23
- render_response Builder.build_to_string(path_to_build)
23
+ bundle = nil
24
+ if Proscenium.config.external_node_modules && path_to_build.start_with?('node_modules/')
25
+ bundle = false
26
+ end
27
+
28
+ render_response Builder.build_to_string(path_to_build, bundle:)
24
29
  rescue Builder::CompileError => e
25
30
  raise self.class::CompileError, { file: @request.fullpath, detail: e.message }, caller
26
31
  end
@@ -10,7 +10,6 @@ module Proscenium
10
10
  autoload :Base
11
11
  autoload :Esbuild
12
12
  autoload :Engines
13
- autoload :Runtime
14
13
 
15
14
  def initialize(app)
16
15
  @app = app
@@ -40,7 +39,6 @@ module Proscenium
40
39
  end
41
40
 
42
41
  def find_type(request)
43
- return Runtime if request.path.match?(%r{^/@proscenium/})
44
42
  return Esbuild if Pathname.new(request.path).fnmatch?(app_path_glob, File::FNM_EXTGLOB)
45
43
 
46
44
  pathname = Pathname.new(request.path)
@@ -53,12 +51,12 @@ module Proscenium
53
51
  end
54
52
 
55
53
  def engines_path_glob
56
- names = Proscenium.config.engines.map(&:engine_name)
54
+ names = Proscenium.config.engines.keys
57
55
  "/{#{names.join(',')}}/{#{Proscenium::ALLOWED_DIRECTORIES}}/**.{#{file_extensions}}"
58
56
  end
59
57
 
60
58
  def ui_path_glob
61
- "/proscenium/ui/**.{#{file_extensions}}"
59
+ "/proscenium/**.{#{file_extensions}}"
62
60
  end
63
61
 
64
62
  def file_extensions
@@ -14,6 +14,7 @@ module Proscenium
14
14
  config.proscenium.bundle = true
15
15
  config.proscenium.side_load = true
16
16
  config.proscenium.code_splitting = true
17
+ config.proscenium.external_node_modules = false
17
18
 
18
19
  # Cache asset paths when building to path. Enabled by default in production.
19
20
  # @see Proscenium::Builder#build_to_path
@@ -23,6 +24,13 @@ module Proscenium
23
24
  config.proscenium.cache_query_string = Rails.env.production? && ENV.fetch('REVISION', nil)
24
25
  config.proscenium.cache_max_age = 2_592_000 # 30 days
25
26
 
27
+ # A proc that will be given the path to build, and should return a boolean indicating whether to
28
+ # cache the response.
29
+ #
30
+ # Example:
31
+ # cache_middleware_response = ->(path) { path.start_with?('node_modules/') }
32
+ config.proscenium.cache_middleware_response = nil
33
+
26
34
  # List of environment variable names that should be passed to the builder, which will then be
27
35
  # passed to esbuild's `Define` option. Being explicit about which environment variables are
28
36
  # defined means a faster build, as esbuild will have less to do.
@@ -36,9 +44,11 @@ module Proscenium
36
44
  #
37
45
  # Example:
38
46
  # class Gem1::Engine < ::Rails::Engine
39
- # config.proscenium.engines << self
47
+ # config.proscenium.engines[:gem1] = root
40
48
  # end
41
- config.proscenium.engines = Set.new
49
+ config.proscenium.engines = {
50
+ proscenium: Proscenium.ui_path
51
+ }
42
52
 
43
53
  config.action_dispatch.rescue_templates = {
44
54
  'Proscenium::Builder::BuildError' => 'build_error'
@@ -64,9 +74,9 @@ module Proscenium
64
74
  end
65
75
 
66
76
  initializer 'proscenium.middleware' do |app|
67
- app.middleware.insert_after ActionDispatch::Static, Middleware
68
- app.middleware.insert_after ActionDispatch::Static, Rack::ETag, 'no-cache'
69
- app.middleware.insert_after ActionDispatch::Static, Rack::ConditionalGet
77
+ app.middleware.insert_after ActionDispatch::Static, Proscenium::Middleware
78
+ # app.middleware.insert_after ActionDispatch::Static, Rack::ETag, 'no-cache'
79
+ # app.middleware.insert_after ActionDispatch::Static, Rack::ConditionalGet
70
80
  end
71
81
 
72
82
  initializer 'proscenium.sideloading' do
@@ -40,7 +40,7 @@ module Proscenium
40
40
  class_attribute :loader
41
41
 
42
42
  # @return [String] the URL path to the component manager.
43
- class_attribute :manager, default: '/@proscenium/react-manager/index.jsx'
43
+ class_attribute :manager, default: '/proscenium/react-manager/index.jsx'
44
44
  end
45
45
 
46
46
  class_methods do
@@ -11,8 +11,6 @@ module Proscenium
11
11
  #
12
12
  # @param path [String] Can be URL path, file system path, or bare specifier (ie. NPM package).
13
13
  # @return [String] URL path.
14
- #
15
- # rubocop:disable Metrics/*
16
14
  def self.resolve(path)
17
15
  self.resolved ||= {}
18
16
 
@@ -21,12 +19,10 @@ module Proscenium
21
19
  raise ArgumentError, 'path must be an absolute file system or URL path'
22
20
  end
23
21
 
24
- if path.start_with?('@proscenium/')
22
+ if path.start_with?('proscenium/')
25
23
  "/#{path}"
26
- elsif path.start_with?(Proscenium.ui_path.to_s)
27
- path.delete_prefix Proscenium.root.join('lib').to_s
28
- elsif (engine = Proscenium.config.engines.find { |e| path.start_with? "#{e.root}/" })
29
- path.sub(/^#{engine.root}/, "/#{engine.engine_name}")
24
+ elsif (engine = Proscenium.config.engines.find { |_, v| path.start_with? "#{v}/" })
25
+ path.sub(/^#{engine.last}/, "/#{engine.first}")
30
26
  elsif path.start_with?("#{Rails.root}/")
31
27
  path.delete_prefix Rails.root.to_s
32
28
  else
@@ -34,6 +30,5 @@ module Proscenium
34
30
  end
35
31
  end
36
32
  end
37
- # rubocop:enable Metrics/*
38
33
  end
39
34
  end
@@ -172,7 +172,7 @@ module Proscenium
172
172
  end
173
173
 
174
174
  # The reason why we sideload CSS after JS is because the order of CSS is important.
175
- # Basically, the layout should be loaded before the view so that CSS cascading works i9n the
175
+ # Basically, the layout should be loaded before the view so that CSS cascading works in the
176
176
  # right direction.
177
177
  css_imports.reverse_each do |it|
178
178
  Importer.sideload_css it, **options
@@ -0,0 +1 @@
1
+ @import "https://cdn.jsdelivr.net/npm/sourdough-toast/src/sourdough-toast.css";
@@ -0,0 +1,73 @@
1
+ import domMutations from "https://esm.run/dom-mutations";
2
+ import { Sourdough, toast } from "https://esm.run/sourdough-toast";
3
+
4
+ class HueFlash extends HTMLElement {
5
+ static observedAttributes = ["data-flash-alert", "data-flash-notice"];
6
+
7
+ connectedCallback() {
8
+ this.#initSourdough();
9
+ }
10
+
11
+ async #initSourdough() {
12
+ if ("sourdoughBooted" in window) return;
13
+
14
+ const sourdough = new Sourdough({
15
+ richColors: true,
16
+ yPosition: "bottom",
17
+ xPosition: "center",
18
+ });
19
+ sourdough.boot();
20
+ window.sourdoughBooted = true;
21
+
22
+ // Watch for changes to htl:flashes meta tag
23
+ const flashesSelector = "meta[name='rails:flashes']";
24
+ for await (const mutation of domMutations(document.head, {
25
+ childList: true,
26
+ subtree: true,
27
+ attributes: true,
28
+ })) {
29
+ let $ele = null;
30
+
31
+ if (
32
+ mutation.type === "attributes" &&
33
+ mutation.target.nodeName == "META" &&
34
+ mutation.attributeName == "content"
35
+ ) {
36
+ $ele = mutation.target;
37
+ } else if (mutation.type === "childList") {
38
+ for (const node of mutation.addedNodes) {
39
+ if (node.matches(flashesSelector)) {
40
+ $ele = node;
41
+ break;
42
+ }
43
+ }
44
+ }
45
+
46
+ if ($ele) {
47
+ const flashes = JSON.parse($ele.getAttribute("content"));
48
+ for (const [type, message] of Object.entries(flashes)) {
49
+ if (type === "alert") {
50
+ toast.error(message);
51
+ } else if (type === "notice") {
52
+ toast.success(message);
53
+ }
54
+ }
55
+ }
56
+ }
57
+ }
58
+
59
+ attributeChangedCallback(name, _oldValue, newValue) {
60
+ this.#initSourdough();
61
+
62
+ if (newValue === null) return;
63
+
64
+ if (name === "data-flash-alert") {
65
+ toast.warning(newValue);
66
+ } else if (name === "data-flash-notice") {
67
+ toast.success(newValue);
68
+ }
69
+ }
70
+ }
71
+
72
+ !customElements.get("pui-flash") &&
73
+ customElements.define("pui-flash", HueFlash);
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Proscenium::UI
4
+ class Flash < Component
5
+ register_element :pui_flash
6
+
7
+ def self.source_path
8
+ super / '../flash/index.rb'
9
+ end
10
+
11
+ def view_template
12
+ pui_flash data: { flash: helpers.flash.to_hash }
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Proscenium::UI::Form
4
+ module FieldMethods
5
+ # Renders a hidden input field.
6
+ #
7
+ # @param args [Array<Symbol>] name or nested names of model attribute
8
+ # @param attributes [Hash] passed through to each input
9
+ def hidden_field(*args, **)
10
+ render Fields::Hidden.new(args, @model, self, **)
11
+ end
12
+
13
+ # @param args [Array<Symbol>] name or nested names of model attribute
14
+ # @param attributes [Hash] passed through to each input
15
+ def rich_textarea_field(*args, **attributes)
16
+ merge_bang_attributes! args, attributes
17
+ render Fields::RichTextarea.new(args, @model, self, **attributes)
18
+ end
19
+
20
+ # @param args [Array<Symbol>] name or nested names of model attribute
21
+ # @param attributes [Hash] passed through to each input
22
+ def datetime_local_field(*args, **attributes)
23
+ merge_bang_attributes! args, attributes
24
+ render Fields::Datetime.new(args, @model, self, **attributes)
25
+ end
26
+
27
+ # @param args [Array<Symbol>] name or nested names of model attribute
28
+ # @param attributes [Hash] passed through to each input
29
+ def checkbox_field(*args, **attributes)
30
+ merge_bang_attributes! args, attributes
31
+ render Fields::Checkbox.new(args, @model, self, **attributes)
32
+ end
33
+
34
+ # @param args [Array<Symbol>] name or nested names of model attribute
35
+ # @param attributes [Hash] passed through to each input
36
+ def tel_field(*args, **attributes)
37
+ merge_bang_attributes! args, attributes
38
+ render Fields::Tel.new(args, @model, self, **attributes)
39
+ end
40
+
41
+ # @param args [Array<Symbol>] name or nested names of model attribute
42
+ # @param attributes [Hash] passed through to each input
43
+ def select_field(*args, **attributes, &)
44
+ merge_bang_attributes! args, attributes, additional_bang_attrs: [:typeahead]
45
+ render Fields::Select.new(args, @model, self, **attributes, &)
46
+ end
47
+
48
+ # @see #select_field
49
+ def select_country_field(*args, **attributes)
50
+ merge_bang_attributes! args, attributes
51
+ attributes[:typeahead] = true
52
+ attributes[:options] = '/countries'
53
+ attributes[:component_props] = {
54
+ items_on_search: true,
55
+ input_props: { required: attributes.delete(:required) }
56
+ }
57
+
58
+ select_field(*args, **attributes)
59
+ end
60
+
61
+ # Renders a <textarea> field for the given `attribute`.
62
+ #
63
+ # @param args [Array<Symbol>] name or nested names of model attribute
64
+ # @param attributes [Hash] passed through to each input
65
+ def textarea_field(*args, **attributes)
66
+ merge_bang_attributes! args, attributes
67
+ render Fields::Textarea.new(args, @model, self, **attributes)
68
+ end
69
+
70
+ # Renders a group of radio inputs for each option of the given `field`.
71
+ #
72
+ # @param args [Array<Symbol>] name or nested names of model attribute
73
+ # @param attributes [Hash] passed through to each input
74
+ def radio_group(*args, **attributes)
75
+ attributes[:options] = args.pop if args.last.is_a?(Array)
76
+
77
+ render Fields::RadioGroup.new(args, @model, self, **attributes)
78
+ end
79
+
80
+ def radio_field(...)
81
+ div { radio_input(...) }
82
+ end
83
+
84
+ def radio_input(*args, **)
85
+ render Fields::RadioInput.new(args, @model, self, **)
86
+ end
87
+ end
88
+ end