Class: ActionView::Helpers::TagHelper::VueBuilder

Inherits:
Object
  • Object
show all
Defined in:
lib/actionview/vue_tag_helper/vue_builder.rb

Overview

VueBuilder generates Vue component markup from a proxy-style builder interface identical to the built-in tag helper:

It is intentionally not a subclass of TagBuilder. The two helpers have different semantics and will diverge further as Vue-specific features are added.

Tag names

Method names are dasherized before use as the HTML tag name (+my_feed_item+ → my-feed-item). The resulting name must be a valid kebab-cased custom-element name: all-lowercase, letters/digits only, and at least one hyphen separating two non-empty segments. Anything else — PascalCase, a plain lowercase word, underscores — raises ArgumentError immediately, before attributes or content are evaluated.

This mirrors the HTML Living Standard requirement that custom element names contain at least one hyphen, which also guarantees they can never collide with current or future built-in HTML elements.

Closing tags

Every tag is emitted with an explicit closing tag. Vue component tags are never self-closing.

Attribute quoting

Standard TagBuilder always uses double-quote delimiters and escapes any literal " inside a value as ". That is safe but verbose — a JSON blob like {"key":"value"} becomes data="{"key":"value"}".

VueBuilder switches to single-quote delimiters whenever the stringified value contains a double-quote, so the same blob is emitted as data='{"key":"value"}'. The delimiter decision is independent of the escape: flag.

Escaping rules when single-quote delimiters are chosen:

&  →  &
<  →  &lt;
>  →  &gt;
'  →  &#39;   (protects the delimiter)
"  →  (unchanged — the whole point)

Values already marked html_safe? are passed through as-is except that a literal \' is escaped when single-quote delimiters are in use.

Typed attributes / v-bind shorthand

Vue components use typed props (+defineProps<{ count: number }>()+). Passing a plain HTML string like count="42" triggers a Vue runtime warning because the prop expects a Number, not a String.

VueBuilder avoids this by inspecting the Ruby value type:

  • String / Symbol — emitted as a plain attribute (no colon).
  • true — emitted as a valueless attribute (+disabled+). Vue interprets attribute presence as a truthy boolean.
  • everything else (+Integer+, Float, BigDecimal, false, Array, Hash, …) — the attribute name is prefixed with : (the v-bind shorthand) and the value is serialised with #to_json. The resulting JSON string is then subject to the normal single/double-quote quoting rules.

The class: key is exempt from the v-bind rule: an Array or Hash value is always flattened into a space-separated CSS token list.

data: and aria: hashes

A Hash passed under the data: or aria: key is expanded into individual prefixed attributes. Values are serialised as follows:

  • String, Symbol — passed through unchanged.
  • BigDecimal — converted with to_s("F") (fixed-point notation, independent of any ActiveSupport monkey-patches).
  • everything else — serialised with #to_json.

Examples:

vue.my_component(label: "Hello")
# => <my-component label="Hello"></my-component>
vue.my_component   # => <my-component></my-component>  ✓
vue.MyComponent    # => <my-component></my-component>  ✓  (PascalCase normalised)
vue.div            # => ArgumentError ("div")          ✗
vue.my_component(count: 42)
# => <my-component :count="42"></my-component>

vue.my_component(items: ["a", "b"])
# => <my-component :items='["a","b"]'></my-component>

vue.b_btn(disabled: true)
# => <b-btn disabled></b-btn>

vue.b_btn(disabled: false)
# => <b-btn :disabled="false"></b-btn>
vue.my_component(data: {items: ["a", "b"]})
# => <my-component data-items='["a","b"]'></my-component>

Defined Under Namespace

Classes: InvalidTagNameError

Instance Method Summary collapse

Constructor Details

#initialize(view_context) ⇒ VueBuilder

Returns a new instance of VueBuilder.



153
154
155
# File 'lib/actionview/vue_tag_helper/vue_builder.rb', line 153

def initialize(view_context)
  @view_context = view_context
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(called, *args, escape: true, **options) ⇒ Object (private)



163
164
165
166
167
168
169
170
# File 'lib/actionview/vue_tag_helper/vue_builder.rb', line 163

def method_missing(called, *args, escape: true, **options, &)
  original = called.name
  name     = original.underscore.dasherize
  raise InvalidTagNameError.new(original, name) unless KEBAB_TAG_RE.match?(name)

  content  = build_inline_content(args, escape, &)
  "<#{name}#{build_tag_options(options, escape)}>#{content}</#{name}>".html_safe # rubocop:disable Rails/OutputSafety
end