Class: Shadcn::ApplicationViewComponent

Inherits:
ViewComponent::Base
  • Object
show all
Includes:
ViewComponentContrib::StyleVariants
Defined in:
app/components/shadcn/application_view_component.rb

Overview

Base class for every ported shadcn/ui component.

It exists to make the React → ViewComponent mapping mechanical:

React                                  Ruby
------------------------------------   ---------------------------------------
cva(base, {variants, defaultVariants})  style { base {} variants {} defaults {} }
cn(...)                                 TailwindMerge, wired as the style postprocessor
data-slot="card-header"                 slot_name :"card-header"
<div {...props} />                      **attributes, splatted through Rails' tag builder
asChild                                 as: :a / as: :span

A component that is a single element carrying data-slot + classes needs no template at all: the inherited #call renders it. Components whose markup is richer (an icon, a nested wrapper, an indicator) get a sidecar component.html.erb, which overrides #call.

Direct Known Subclasses

Shadcn::Accordion::Component, Shadcn::Accordion::Content::Component, Shadcn::Accordion::Item::Component, Shadcn::Accordion::Trigger::Component, Shadcn::Alert::Component, Shadcn::AlertDialog::Action::Component, Shadcn::AlertDialog::Component, Shadcn::AlertDialog::Content::Component, Shadcn::AlertDialog::Header::Component, Shadcn::AspectRatio::Component, Shadcn::Attachment::Component, Shadcn::Attachment::Media::Component, Shadcn::Attachment::Trigger::Component, Shadcn::Avatar::Component, Shadcn::Avatar::Fallback::Component, Shadcn::Avatar::Image::Component, Badge::Component, Breadcrumb::Component, Breadcrumb::Ellipsis::Component, Breadcrumb::Page::Component, Breadcrumb::Separator::Component, Bubble::Component, Bubble::Reactions::Component, Button::Component, ButtonGroup::Component, ButtonGroup::Text::Component, Calendar::Component, Card::Component, Card::Header::Component, Carousel::Component, Carousel::Content::Component, Carousel::Item::Component, Carousel::Next::Component, Carousel::Previous::Component, Chart::Cartesian::Component, Chart::Component, Chart::Legend::Component, Chart::Pie::Component, Chart::Table::Component, Chart::Tooltip::Component, Checkbox::Component, Collapsible::Component, Collapsible::Content::Component, Collapsible::Trigger::Component, Combobox::Chip::Component, Combobox::Chips::Component, Combobox::Component, Combobox::Content::Component, Combobox::Empty::Component, Combobox::Input::Component, Combobox::Item::Component, Combobox::List::Component, Command::Component, Command::Dialog::Component, Command::Empty::Component, Command::Group::Component, Command::Input::Component, Command::Item::Component, Command::List::Component, Command::Separator::Component, ContextMenu::Component, ContextMenu::Content::Component, ContextMenu::Sub::Component, ContextMenu::Trigger::Component, Dialog::Close::Component, Dialog::Component, Dialog::Content::Component, Dialog::Header::Component, Dialog::Overlay::Component, Dialog::Trigger::Component, Drawer::Component, Drawer::Content::Component, Drawer::Header::Component, DropdownMenu::CheckboxItem::Component, DropdownMenu::Component, DropdownMenu::Content::Component, DropdownMenu::Group::Component, DropdownMenu::Item::Component, DropdownMenu::RadioGroup::Component, DropdownMenu::RadioItem::Component, DropdownMenu::Sub::Component, DropdownMenu::SubContent::Component, DropdownMenu::SubTrigger::Component, DropdownMenu::Trigger::Component, Empty::Component, Empty::Media::Component, Field::Component, Field::Error::Component, Field::Legend::Component, Field::Separator::Component, HoverCard::Component, HoverCard::Content::Component, HoverCard::Trigger::Component, Icon::Component, Input::Component, InputGroup::Addon::Component, InputGroup::Component, InputGroup::Text::Component, InputOtp::Component, InputOtp::Separator::Component, InputOtp::Slot::Component, Item::Component, Item::Group::Component, Item::Media::Component, Kbd::Component, Kbd::Group::Component, Label::Component, Marker::Component, Marker::Icon::Component, Menubar::Component, Menubar::Content::Component, Menubar::Menu::Component, Menubar::Sub::Component, Menubar::Trigger::Component, Message::Component, MessageScroller::Component, MessageScroller::Content::Component, MessageScroller::Item::Component, MessageScroller::Viewport::Component, ModeSwitcher::Component, ModeToggle::Component, NativeSelect::Component, NavigationMenu::Component, NavigationMenu::Content::Component, NavigationMenu::Trigger::Component, Pagination::Component, Pagination::Ellipsis::Component, Pagination::Link::Component, Popover::Anchor::Component, Popover::Component, Popover::Content::Component, Popover::Trigger::Component, Progress::Component, RadioGroup::Component, RadioGroup::Item::Component, Resizable::Component, Resizable::Handle::Component, Resizable::Panel::Component, ScrollArea::Component, ScrollArea::Scrollbar::Component, ScrollArea::Viewport::Component, Select::Component, Select::Content::Component, Select::Empty::Component, Select::Group::Component, Select::Item::Component, Select::List::Component, Select::Search::Component, Select::Trigger::Component, Select::Value::Component, Separator::Component, Sheet::Component, Sheet::Content::Component, Sheet::Header::Component, Sidebar::Component, Sidebar::MenuAction::Component, Sidebar::MenuButton::Component, Sidebar::MenuSkeleton::Component, Sidebar::MenuSubButton::Component, Sidebar::Provider::Component, Sidebar::Rail::Component, Sidebar::Trigger::Component, Skeleton::Component, Slider::Component, Slider::Range::Component, Slider::Thumb::Component, Slider::Track::Component, Switch::Component, Table::Component, Tabs::Component, Tabs::Content::Component, Tabs::List::Component, Tabs::Trigger::Component, Textarea::Component, ThemeSelector::Component, Toaster::Component, Toaster::Toast::Component, Toggle::Component, ToggleGroup::Component, ToggleGroup::Item::Component, Tooltip::Component, Tooltip::Content::Component, Tooltip::Provider::Component, Tooltip::Trigger::Component

Constant Summary collapse

VOID_TAGS =

Elements that must not be given a block/closing tag.

%i[area base br col embed hr img input link meta source track wbr].freeze
CONTENTS_STYLE =

Radix's context-only roots (Dialog, Popover, Select, …) render no element at all. We need somewhere to hang the Stimulus controller, so those roots emit a wrapper that is invisible to layout.

"display:contents"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(as: nil, **attributes) ⇒ ApplicationViewComponent

Returns a new instance of ApplicationViewComponent.

Parameters:

  • as (Symbol, String, nil) (defaults to: nil)

    render a different element (shadcn's asChild)

  • attributes (Hash)

    anything else is forwarded to the element, like {...props}



52
53
54
55
# File 'app/components/shadcn/application_view_component.rb', line 52

def initialize(as: nil, **attributes)
  @as = as
  @attributes = attributes
end

Instance Attribute Details

#attributesObject (readonly)

Returns the value of attribute attributes.



48
49
50
# File 'app/components/shadcn/application_view_component.rb', line 48

def attributes
  @attributes
end

Class Method Details

.default_tag(value = nil) ⇒ Object

The element rendered by default, i.e. the tag in the TSX source.



32
33
34
35
36
# File 'app/components/shadcn/application_view_component.rb', line 32

def default_tag(value = nil)
  return @default_tag = value.to_sym if value

  @default_tag ||= superclass.respond_to?(:default_tag) ? superclass.default_tag : :div
end

.slot_name(value = nil) ⇒ Object

The data-slot value shadcn stamps on this part. Inherited, so that specialisations such as PaginationPrevious keep PaginationLink's slot.



40
41
42
43
44
45
# File 'app/components/shadcn/application_view_component.rb', line 40

def slot_name(value = nil)
  return @slot_name = value.to_s if value
  return @slot_name if defined?(@slot_name)

  @slot_name = superclass.respond_to?(:slot_name) ? superclass.slot_name : nil
end

Instance Method Details

#callObject



57
58
59
# File 'app/components/shadcn/application_view_component.rb', line 57

def call
  render_element(body: content)
end

#css_classes(extra = nil) ⇒ Object

Compiles the cva-equivalent style set. Caller classes go last so tailwind-merge resolves conflicts in their favour. Parts that carry no classes of their own still have to pass the caller's through.

Without caller classes the result depends only on the component class and its variants, and the style blocks are static once loaded — so it is computed once. That is not just a saving on the merge: it keeps the library's own class strings out of tailwind_merge's LRU, which is a fixed size shared with everything the caller varies.



101
102
103
104
105
106
107
# File 'app/components/shadcn/application_view_component.rb', line 101

def css_classes(extra = nil)
  return compile_classes(extra) if extra.present?

  ShadcnViewComponent.class_cache.fetch_or_store([ self.class.name, style_variants ]) do
    compile_classes(nil)
  end
end

#element_attributes(**defaults) ⇒ Object

The full attribute hash for the root element.

Precedence, lowest to highest: the data-slot marker, then the component's own defaults (what subclasses pass up through super), then whatever the caller gave us. The caller winning is the point — it is what {...props} does in every shadcn component, where the spread comes last.



85
86
87
88
89
90
# File 'app/components/shadcn/application_view_component.rb', line 85

def element_attributes(**defaults)
  base = { "data-slot" => self.class.slot_name }.compact
  merged = merge_attributes(base, defaults, attributes)
  merged["class"] = css_classes(merged.delete("class"))
  merged.compact
end

#merged_style(*declarations) ⇒ Object

Combines inline styles without dropping whatever the caller passed.



136
137
138
# File 'app/components/shadcn/application_view_component.rb', line 136

def merged_style(*declarations)
  [ attributes[:style], *declarations ].compact.reject(&:empty?).join(";").presence
end

#render_element(body: nil, **defaults, &block) ⇒ Object

Renders the component's root element. defaults are the attributes the component itself contributes; see #element_attributes for how they rank against the caller's.

The body comes either from a block (templates: <%= render_element do %>) or from body:#call passes ViewComponent's already-captured content that way, because re-capturing it here would swallow it.



72
73
74
75
76
77
# File 'app/components/shadcn/application_view_component.rb', line 72

def render_element(body: nil, **defaults, &block)
  attrs = element_attributes(**defaults)
  return tag.public_send(tag_name, **attrs) if VOID_TAGS.include?(tag_name)

  (tag_name, block ? capture(&block) : body, attrs)
end

#shadcn_t(key, **interpolations) ⇒ Object

The user-visible strings the components render, looked up under shadcn_view_component.* and falling back to the bundled English — so the gem works untranslated and a host app can override any key.

The fallback is the locale: :en default below, and it has to be there. This method used to be a bare I18n.t while the comment above it already claimed the English was a default; it was not, and the gem only looked untranslatable-proof because every app it had been tried in ran in English. A host running it got I18n::MissingTranslationData out of a searchable select — and with config.i18n.raise_on_missing_translations, which the Rails generators turn on in development and test, a raised page rather than a fallback string.

A lambda, so the second lookup only happens when the first misses.



128
129
130
131
132
133
# File 'app/components/shadcn/application_view_component.rb', line 128

def shadcn_t(key, **interpolations)
  full_key = "shadcn_view_component.#{key}"

  I18n.t(full_key, **interpolations,
         default: ->(*) { I18n.t(full_key, locale: :en, **interpolations) })
end

#style_variantsObject

Overridden by components that expose variants (button, badge, alert, …).



110
111
112
# File 'app/components/shadcn/application_view_component.rb', line 110

def style_variants
  {}
end

#tag_nameObject



61
62
63
# File 'app/components/shadcn/application_view_component.rb', line 61

def tag_name
  (@as || self.class.default_tag).to_sym
end