Class: JsxRosetta::Backend::ViewComponent

Inherits:
Base
  • Object
show all
Defined in:
lib/jsx_rosetta/backend/view_component.rb,
lib/jsx_rosetta/backend/view_component/expression_translator.rb

Overview

Emits a Rails ViewComponent (one Ruby class + one ERB template) from an IR::Component.

Phase 3 scope:

- Single component per emit.
- JSX prop names lowered to snake_case Ruby kwargs and matching
  `@instance_variable` assignments.
- JS expressions translated via ExpressionTranslator where the
  shape is recognized; otherwise emitted as a TODO marker plus
  verbatim source.
- HTML attributes emitted directly; className / template-literal
  class expressions inlined into the `class="..."` attribute.

Phase 4a additions:

- `children` prop is treated as ViewComponent's default content
  slot: it's filtered out of the initializer and rendered as
  `<%= content %>` wherever the IR has IR::Slot(name: "children").
- IR::Conditional renders as `<% if %>...<% else %>...<% end %>`.

Direct Known Subclasses

RailsView

Defined Under Namespace

Classes: ExpressionTranslator

Constant Summary collapse

DEFAULT_SLOT_NAME =
"children"
VOID_ELEMENTS =
%w[area base br col embed hr img input link meta param source track wbr].freeze
DEFAULT_HELPERS =

JSX component names that have a direct Rails view-helper analog. Override per-instance via ‘ViewComponent.new(helpers: …)`, or disable by passing `helpers: false`.

{
  "Link" => { method: :link_to, positional: :href }.freeze,
  "Image" => { method: :image_tag, positional: :src }.freeze
}.freeze

Instance Method Summary collapse

Constructor Details

#initialize(helpers: nil, layout: :sidecar) ⇒ ViewComponent

Returns a new instance of ViewComponent.



39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/jsx_rosetta/backend/view_component.rb', line 39

def initialize(helpers: nil, layout: :sidecar)
  super()
  @helpers = case helpers
             when nil then DEFAULT_HELPERS
             when false then {}
             else helpers
             end
  unless %i[sidecar flat].include?(layout)
    raise ArgumentError, "unknown layout: #{layout.inspect} (expected :sidecar or :flat)"
  end

  @layout = layout
end

Instance Method Details

#emit(component) ⇒ Object



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/jsx_rosetta/backend/view_component.rb', line 53

def emit(component)
  prop_names = component.props.map(&:name)
  prop_names << component.rest_prop_name if component.rest_prop_name
  translator = ExpressionTranslator.new(prop_names: prop_names)

  base_name = "#{AST::Inflector.underscore(component.name)}_component"
  @stimulus_identifier = component.stimulus_methods.any? ? stimulus_identifier(component) : nil

  files = [
    File.new(path: "#{base_name}.rb", contents: render_ruby_class(component, translator)),
    File.new(path: erb_path(base_name), contents: render_erb_template(component, translator))
  ]
  if component.stimulus_methods.any?
    files << File.new(
      path: stimulus_path(component, base_name),
      contents: render_stimulus_controller_js(component)
    )
  end
  files
end

#erb_path(base_name) ⇒ Object



74
75
76
# File 'lib/jsx_rosetta/backend/view_component.rb', line 74

def erb_path(base_name)
  @layout == :sidecar ? "#{base_name}/#{base_name}.html.erb" : "#{base_name}.html.erb"
end

#render_stimulus_controller_js(component) ⇒ Object



87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/jsx_rosetta/backend/view_component.rb', line 87

def render_stimulus_controller_js(component)
  lines = [
    'import { Controller } from "@hotwired/stimulus";',
    "",
    "export default class extends Controller {"
  ]
  component.stimulus_methods.each_with_index do |method, idx|
    lines << "" if idx.positive?
    lines.concat(stimulus_method_lines(method))
  end
  lines << "}"
  "#{lines.join("\n")}\n"
end

#stimulus_identifier(component) ⇒ Object



83
84
85
# File 'lib/jsx_rosetta/backend/view_component.rb', line 83

def stimulus_identifier(component)
  AST::Inflector.underscore(component.name).tr("_", "-")
end

#stimulus_method_lines(method) ⇒ Object



101
102
103
104
105
106
107
108
109
# File 'lib/jsx_rosetta/backend/view_component.rb', line 101

def stimulus_method_lines(method)
  body_lines = method.body_source.strip.split("\n")
  commented = body_lines.map { |line| "  //   #{line}" }
  ["  // TODO: translate from the original JSX handler:"] + commented + [
    "  #{method.name}(event) {",
    "    // ...",
    "  }"
  ]
end

#stimulus_path(component, base_name) ⇒ Object



78
79
80
81
# File 'lib/jsx_rosetta/backend/view_component.rb', line 78

def stimulus_path(component, base_name)
  controller_filename = "#{AST::Inflector.underscore(component.name)}_controller.js"
  @layout == :sidecar ? "#{base_name}/#{controller_filename}" : controller_filename
end