Saturday, October 19, 2024

Glimmer DSL for Web Component Custom Event Listeners / Component Slots / Default Slot

Glimmer DSL for Web (Ruby-in-the-Browser Web Frontend Framework) recently added support for Component Custom Event Listeners, Component Slots, and Default Slot in the 0.5.x & 0.6.x version releases. The new samples Hello, Component Slots!, Hello, Component Listeners!, and Hello, Component Listeners (Default Slot)! have been added as well and can be played around with in an online hosted Rails Sample Selector web app (the app takes a while to load because of cheap hosting, but once it loads, it is fast in the Frontend).

Component Slots is a feature that enables consumers of a component (e.g. address form) to contribute visual elements in one or multiple slots within the component (e.g. address form header and footer), thus facilitating extension of components following the Open/Closed Principle. The consumer code would just open a block matching the component slot name inside the content block of the consumed component.

Hello, Component Slots! is a new sample that demonstrates Component Slots. Notice how the 2 address forms are augmented with a header and footer inserted within their content.



require 'glimmer-dsl-web'
Address = Struct.new(:full_name, :street, :street2, :city, :state, :zip_code, :billing_and_shipping, keyword_init: true) do
STATES = {
"AK"=>"Alaska", "AL"=>"Alabama", "AR"=>"Arkansas", "AS"=>"American Samoa", "AZ"=>"Arizona",
"CA"=>"California", "CO"=>"Colorado", "CT"=>"Connecticut", "DC"=>"District of Columbia", "DE"=>"Delaware",
"FL"=>"Florida", "GA"=>"Georgia", "GU"=>"Guam", "HI"=>"Hawaii", "IA"=>"Iowa", "ID"=>"Idaho", "IL"=>"Illinois",
"IN"=>"Indiana", "KS"=>"Kansas", "KY"=>"Kentucky", "LA"=>"Louisiana", "MA"=>"Massachusetts", "MD"=>"Maryland",
"ME"=>"Maine", "MI"=>"Michigan", "MN"=>"Minnesota", "MO"=>"Missouri", "MS"=>"Mississippi", "MT"=>"Montana",
"NC"=>"North Carolina", "ND"=>"North Dakota", "NE"=>"Nebraska", "NH"=>"New Hampshire", "NJ"=>"New Jersey",
"NM"=>"New Mexico", "NV"=>"Nevada", "NY"=>"New York", "OH"=>"Ohio", "OK"=>"Oklahoma", "OR"=>"Oregon",
"PA"=>"Pennsylvania", "PR"=>"Puerto Rico", "RI"=>"Rhode Island", "SC"=>"South Carolina", "SD"=>"South Dakota",
"TN"=>"Tennessee", "TX"=>"Texas", "UT"=>"Utah", "VA"=>"Virginia", "VI"=>"Virgin Islands", "VT"=>"Vermont",
"WA"=>"Washington", "WI"=>"Wisconsin", "WV"=>"West Virginia", "WY"=>"Wyoming"
}
def state_code
STATES.invert[state]
end
def state_code=(value)
self.state = STATES[value]
end
def summary
string_attributes = to_h.except(:billing_and_shipping)
summary = string_attributes.values.map(&:to_s).reject(&:empty?).join(', ')
summary += " (Billing & Shipping)" if billing_and_shipping
summary
end
end
# AddressForm Glimmer Web Component (View component)
#
# Including Glimmer::Web::Component makes this class a View component and automatically
# generates a new Glimmer HTML DSL keyword that matches the lowercase underscored version
# of the name of the class. AddressForm generates address_form_with_slots keyword, which can be used
# elsewhere in Glimmer HTML DSL code as done inside HelloComponentSlots below.
class AddressFormWithSlots
include Glimmer::Web::Component
option :address
# markup block provides the content of the
markup {
div {
# designate this div as a slot with the slot name :address_header to enable
# consumers to contribute elements to `address_header {...}` slot
div(slot: :address_header, class: 'address-form-header')
div(class: 'address-field-container', style: {display: :grid, grid_auto_columns: '80px 260px'}) {
label('Full Name: ', for: 'full-name-field')
input(id: 'full-name-field') {
value <=> [address, :full_name]
}
@somelabel = label('Street: ', for: 'street-field')
input(id: 'street-field') {
value <=> [address, :street]
}
label('Street 2: ', for: 'street2-field')
textarea(id: 'street2-field') {
value <=> [address, :street2]
}
label('City: ', for: 'city-field')
input(id: 'city-field') {
value <=> [address, :city]
}
label('State: ', for: 'state-field')
select(id: 'state-field') {
Address::STATES.each do |state_code, state|
option(value: state_code) { state }
end
value <=> [address, :state_code]
}
label('Zip Code: ', for: 'zip-code-field')
input(id: 'zip-code-field', type: 'number', min: '0', max: '99999') {
value <=> [address, :zip_code,
on_write: :to_s,
]
}
}
div(style: {margin: 5}) {
inner_text <= [address, :summary,
computed_by: address.members + ['state_code'],
]
}
# designate this div as a slot with the slot name :address_footer to enable
# consumers to contribute elements to `address_footer {...}` slot
div(slot: :address_footer, class: 'address-form-footer')
}
}
style {
r('.address-field-container *') {
margin 5
}
r('.address-field-container input, .address-field-container select') {
grid_column '2'
}
}
end
# HelloComponentSlots Glimmer Web Component (View component)
#
# This View component represents the main page being rendered,
# as done by its `render` class method below
class HelloComponentSlots
include Glimmer::Web::Component
before_render do
@shipping_address = Address.new(
full_name: 'Johnny Doe',
street: '3922 Park Ave',
street2: 'PO BOX 8382',
city: 'San Diego',
state: 'California',
zip_code: '91913',
)
@billing_address = Address.new(
full_name: 'John C Doe',
street: '123 Main St',
street2: 'Apartment 3C',
city: 'San Diego',
state: 'California',
zip_code: '91911',
)
end
markup {
div {
address_form_with_slots(address: @shipping_address) {
address_header { # contribute elements to the address_header component slot
h1('Shipping Address')
legend('This is the address that is used for shipping your purchase.', style: {margin_bottom: 10})
}
address_footer { # contribute elements to the address_footer component slot
p(sub("#{strong('Note:')} #{em('Purchase will be returned if the Shipping Address does not accept it in one week.')}"))
}
}
address_form_with_slots(address: @billing_address) {
address_header { # contribute elements to the address_header component slot
h1('Billing Address')
legend('This is the address that is used for your billing method (e.g. credit card).', style: {margin_bottom: 10})
}
address_footer { # contribute elements to the address_footer component slot
p(sub("#{strong('Note:')} #{em('Payment will fail if payment method does not match the Billing Address.')}"))
}
}
}
}
end
Document.ready? do
# renders a top-level (root) HelloComponentSlots component
HelloComponentSlots.render
end

Component Custom Event Listeners is a feature that enables component consumers to add listeners for custom events that are specific to certain components. Those events can be declared at the top of a component class via the `events :event1, :event2, ...` method. For example, an accordion component can support `on_accordion_section_expanded` and `on_accordion_section_collapsed` custom event listeners (via `events :events :accordion_section_expanded, :accordion_section_collapsed`), which consumers could hook into via `on_eventxyz` blocks in order to do some work when those events fire.

Hello, Components Listeners! is a new sample that demonstrates Component Custom Event Listeners with an `accordion` component that has expandable/collapsable sections. Each AccordionSection supports on_expanded and on_collapsed event listeners, and the full Accordion supports on_accordion_section_expanded and on_accordion_section_collapsed event listeners.

require 'glimmer-dsl-web'
Address = Struct.new(:full_name, :street, :street2, :city, :state, :zip_code, :billing_and_shipping, keyword_init: true) do
STATES = {
"AK"=>"Alaska", "AL"=>"Alabama", "AR"=>"Arkansas", "AS"=>"American Samoa", "AZ"=>"Arizona",
"CA"=>"California", "CO"=>"Colorado", "CT"=>"Connecticut", "DC"=>"District of Columbia", "DE"=>"Delaware",
"FL"=>"Florida", "GA"=>"Georgia", "GU"=>"Guam", "HI"=>"Hawaii", "IA"=>"Iowa", "ID"=>"Idaho", "IL"=>"Illinois",
"IN"=>"Indiana", "KS"=>"Kansas", "KY"=>"Kentucky", "LA"=>"Louisiana", "MA"=>"Massachusetts", "MD"=>"Maryland",
"ME"=>"Maine", "MI"=>"Michigan", "MN"=>"Minnesota", "MO"=>"Missouri", "MS"=>"Mississippi", "MT"=>"Montana",
"NC"=>"North Carolina", "ND"=>"North Dakota", "NE"=>"Nebraska", "NH"=>"New Hampshire", "NJ"=>"New Jersey",
"NM"=>"New Mexico", "NV"=>"Nevada", "NY"=>"New York", "OH"=>"Ohio", "OK"=>"Oklahoma", "OR"=>"Oregon",
"PA"=>"Pennsylvania", "PR"=>"Puerto Rico", "RI"=>"Rhode Island", "SC"=>"South Carolina", "SD"=>"South Dakota",
"TN"=>"Tennessee", "TX"=>"Texas", "UT"=>"Utah", "VA"=>"Virginia", "VI"=>"Virgin Islands", "VT"=>"Vermont",
"WA"=>"Washington", "WI"=>"Wisconsin", "WV"=>"West Virginia", "WY"=>"Wyoming"
}
def state_code
STATES.invert[state]
end
def state_code=(value)
self.state = STATES[value]
end
def summary
string_attributes = to_h.except(:billing_and_shipping)
summary = string_attributes.values.map(&:to_s).reject(&:empty?).join(', ')
summary += " (Billing & Shipping)" if billing_and_shipping
summary
end
end
# AddressForm Glimmer Web Component (View component)
#
# Including Glimmer::Web::Component makes this class a View component and automatically
# generates a new Glimmer HTML DSL keyword that matches the lowercase underscored version
# of the name of the class. AddressForm generates address_form keyword, which can be used
# elsewhere in Glimmer HTML DSL code as done inside HelloComponentListeners below.
class AddressForm
include Glimmer::Web::Component
option :address
markup {
div {
div(style: {display: :grid, grid_auto_columns: '80px 260px'}) { |address_div|
label('Full Name: ', for: 'full-name-field')
input(id: 'full-name-field') {
value <=> [address, :full_name]
}
label('Street: ', for: 'street-field')
input(id: 'street-field') {
value <=> [address, :street]
}
label('Street 2: ', for: 'street2-field')
textarea(id: 'street2-field') {
value <=> [address, :street2]
}
label('City: ', for: 'city-field')
input(id: 'city-field') {
value <=> [address, :city]
}
label('State: ', for: 'state-field')
select(id: 'state-field') {
Address::STATES.each do |state_code, state|
option(value: state_code) { state }
end
value <=> [address, :state_code]
}
label('Zip Code: ', for: 'zip-code-field')
input(id: 'zip-code-field', type: 'number', min: '0', max: '99999') {
value <=> [address, :zip_code,
on_write: :to_s,
]
}
style {
r("#{address_div.selector} *") {
margin '5px'
}
r("#{address_div.selector} input, #{address_div.selector} select") {
grid_column '2'
}
}
}
div(style: {margin: 5}) {
inner_text <= [address, :summary,
computed_by: address.members + ['state_code'],
]
}
}
}
end
class AccordionSection
class Presenter
attr_accessor :collapsed, :instant_transition
def toggle_collapsed(instant: false)
self.instant_transition = instant
self.collapsed = !collapsed
end
def expand(instant: false)
self.instant_transition = instant
self.collapsed = false
end
def collapse(instant: false)
self.instant_transition = instant
self.collapsed = true
end
end
include Glimmer::Web::Component
events :expanded, :collapsed
option :title
attr_reader :presenter
before_render do
@presenter = Presenter.new
end
markup {
section {
# Unidirectionally data-bind the class inclusion of 'collapsed' to the @presenter.collapsed boolean attribute,
# meaning if @presenter.collapsed changes to true, the CSS class 'collapsed' is included on the element,
# and if it changes to false, the CSS class 'collapsed' is removed from the element.
class_name(:collapsed) <= [@presenter, :collapsed]
class_name(:instant_transition) <= [@presenter, :instant_transition]
header(title, class: 'accordion-section-title') {
onclick do |event|
@presenter.toggle_collapsed
if @presenter.collapsed
notify_listeners(:collapsed)
else
notify_listeners(:expanded)
end
end
}
div(slot: :section_content, class: 'accordion-section-content')
}
}
style {
r('.accordion-section-title') {
font_size 2.em
font_weight :bold
cursor :pointer
padding_left 20
position :relative
margin_block_start 0.33.em
margin_block_end 0.33.em
}
r('.accordion-section-title::before') {
content '"▼"'
position :absolute
font_size 0.5.em
top 10
left 0
}
r('.accordion-section-content') {
height 246
overflow :hidden
transition 'height 0.5s linear'
}
r("#{component_element_selector}.instant_transition .accordion-section-content") {
transition 'initial'
}
r("#{component_element_selector}.collapsed .accordion-section-title::before") {
content '"►"'
}
r("#{component_element_selector}.collapsed .accordion-section-content") {
height 0
}
}
end
class Accordion
include Glimmer::Web::Component
events :accordion_section_expanded, :accordion_section_collapsed
markup {
# given that no slots are specified, nesting content under the accordion component
# in consumer code adds content directly inside the markup root div.
div { |accordion|
# on render, all accordion sections would have been added by consumers already, so we can
# attach listeners to all of them by re-opening their content with `.content { ... }` block
on_render do
accordion_section_elements = accordion.children
accordion_sections = accordion_section_elements.map(&:component)
accordion_sections.each_with_index do |accordion_section, index|
accordion_section_number = index + 1
# ensure only the first section is expanded
accordion_section.presenter.collapse(instant: true) if accordion_section_number != 1
accordion_section.content {
on_expanded do
other_accordion_sections = accordion_sections.reject {|other_accordion_section| other_accordion_section == accordion_section }
other_accordion_sections.each { |other_accordion_section| other_accordion_section.presenter.collapse }
notify_listeners(:accordion_section_expanded, accordion_section_number)
end
on_collapsed do
notify_listeners(:accordion_section_collapsed, accordion_section_number)
end
}
end
end
}
}
end
# HelloComponentListeners Glimmer Web Component (View component)
#
# This View component represents the main page being rendered,
# as done by its `render` class method below
#
# Note: check out HelloComponentListenersDefaultSlot for a simpler version that leverages the default slot feature
class HelloComponentListeners
class Presenter
attr_accessor :status_message
def initialize
@status_message = "Accordion section 1 is expanded!"
end
end
include Glimmer::Web::Component
before_render do
@presenter = Presenter.new
@shipping_address = Address.new(
full_name: 'Johnny Doe',
street: '3922 Park Ave',
street2: 'PO BOX 8382',
city: 'San Diego',
state: 'California',
zip_code: '91913',
)
@billing_address = Address.new(
full_name: 'John C Doe',
street: '123 Main St',
street2: 'Apartment 3C',
city: 'San Diego',
state: 'California',
zip_code: '91911',
)
@emergency_address = Address.new(
full_name: 'Mary Doe',
street: '2038 Ipswitch St',
street2: 'Suite 300',
city: 'San Diego',
state: 'California',
zip_code: '91912',
)
end
markup {
div {
h1(style: {font_style: :italic}) {
inner_html <= [@presenter, :status_message]
}
accordion { # any content nested under component directly is added under its markup root div element
accordion_section(title: 'Shipping Address') {
section_content { # contribute elements to section_content slot declared in AccordionSection component
address_form(address: @shipping_address)
}
}
accordion_section(title: 'Billing Address') {
section_content {
address_form(address: @billing_address)
}
}
accordion_section(title: 'Emergency Address') {
section_content {
address_form(address: @emergency_address)
}
}
# on_accordion_section_expanded listener matches event :accordion_section_expanded declared in Accordion component
on_accordion_section_expanded { |accordion_section_number|
@presenter.status_message = "Accordion section #{accordion_section_number} is expanded!"
}
on_accordion_section_collapsed { |accordion_section_number|
@presenter.status_message = "Accordion section #{accordion_section_number} is collapsed!"
}
}
}
}
end
Document.ready? do
# renders a top-level (root) HelloComponentListeners component
# Note: check out hello_component_listeners_default_slot.rb for a simpler version that leverages the default slot feature
HelloComponentListeners.render
end

Default Slot: If a Software Engineer knows that a specific component slot will be the one used the most for a certain component (e.g. a slot for inserting content into an accordion), they can designate it as the default slot. That way, by simply adding content inside the content block of a consumed component, it will get added automatically to the default slot.

Hello, Component Listeners (Default Slot)! leverages this feature to simplify the consumer code for the AccordionSection component section_content slot in the Hello, Component Listeners! sample.


require 'glimmer-dsl-web'
Address = Struct.new(:full_name, :street, :street2, :city, :state, :zip_code, keyword_init: true) do
STATES = {
"AK"=>"Alaska",
"AL"=>"Alabama",
"AR"=>"Arkansas",
"AS"=>"American Samoa",
"AZ"=>"Arizona",
"CA"=>"California",
"CO"=>"Colorado",
"CT"=>"Connecticut",
"DC"=>"District of Columbia",
"DE"=>"Delaware",
"FL"=>"Florida",
"GA"=>"Georgia",
"GU"=>"Guam",
"HI"=>"Hawaii",
"IA"=>"Iowa",
"ID"=>"Idaho",
"IL"=>"Illinois",
"IN"=>"Indiana",
"KS"=>"Kansas",
"KY"=>"Kentucky",
"LA"=>"Louisiana",
"MA"=>"Massachusetts",
"MD"=>"Maryland",
"ME"=>"Maine",
"MI"=>"Michigan",
"MN"=>"Minnesota",
"MO"=>"Missouri",
"MS"=>"Mississippi",
"MT"=>"Montana",
"NC"=>"North Carolina",
"ND"=>"North Dakota",
"NE"=>"Nebraska",
"NH"=>"New Hampshire",
"NJ"=>"New Jersey",
"NM"=>"New Mexico",
"NV"=>"Nevada",
"NY"=>"New York",
"OH"=>"Ohio",
"OK"=>"Oklahoma",
"OR"=>"Oregon",
"PA"=>"Pennsylvania",
"PR"=>"Puerto Rico",
"RI"=>"Rhode Island",
"SC"=>"South Carolina",
"SD"=>"South Dakota",
"TN"=>"Tennessee",
"TX"=>"Texas",
"UT"=>"Utah",
"VA"=>"Virginia",
"VI"=>"Virgin Islands",
"VT"=>"Vermont",
"WA"=>"Washington",
"WI"=>"Wisconsin",
"WV"=>"West Virginia",
"WY"=>"Wyoming"
}
def state_code
STATES.invert[state]
end
def state_code=(value)
self.state = STATES[value]
end
def summary
to_h.values.map(&:to_s).reject(&:empty?).join(', ')
end
end
# AddressForm Glimmer Web Component (View component)
#
# Including Glimmer::Web::Component makes this class a View component and automatically
# generates a new Glimmer HTML DSL keyword that matches the lowercase underscored version
# of the name of the class. AddressForm generates address_form keyword, which can be used
# elsewhere in Glimmer HTML DSL code as done inside HelloComponentListenersDefaultSlot below.
class AddressForm
include Glimmer::Web::Component
option :address
markup {
div {
div(style: {display: :grid, grid_auto_columns: '80px 260px'}) { |address_div|
label('Full Name: ', for: 'full-name-field')
input(id: 'full-name-field') {
value <=> [address, :full_name]
}
label('Street: ', for: 'street-field')
input(id: 'street-field') {
value <=> [address, :street]
}
label('Street 2: ', for: 'street2-field')
textarea(id: 'street2-field') {
value <=> [address, :street2]
}
label('City: ', for: 'city-field')
input(id: 'city-field') {
value <=> [address, :city]
}
label('State: ', for: 'state-field')
select(id: 'state-field') {
Address::STATES.each do |state_code, state|
option(value: state_code) { state }
end
value <=> [address, :state_code]
}
label('Zip Code: ', for: 'zip-code-field')
input(id: 'zip-code-field', type: 'number', min: '0', max: '99999') {
value <=> [address, :zip_code,
on_write: :to_s,
]
}
style {
r("#{address_div.selector} *") {
margin '5px'
}
r("#{address_div.selector} input, #{address_div.selector} select") {
grid_column '2'
}
}
}
div(style: {margin: 5}) {
inner_text <= [address, :summary,
computed_by: address.members + ['state_code'],
]
}
}
}
end
# Note: this is similar to AccordionSection in HelloComponentSlots but specifies default_slot for simpler consumption
class AccordionSection2
class Presenter
attr_accessor :collapsed, :instant_transition
def toggle_collapsed(instant: false)
self.instant_transition = instant
self.collapsed = !collapsed
end
def expand(instant: false)
self.instant_transition = instant
self.collapsed = false
end
def collapse(instant: false)
self.instant_transition = instant
self.collapsed = true
end
end
include Glimmer::Web::Component
events :expanded, :collapsed
default_slot :section_content # automatically insert content in this element slot inside markup
option :title
attr_reader :presenter
before_render do
@presenter = Presenter.new
end
markup {
section { # represents the :markup_root_slot to allow inserting content here instead of in default_slot
# Unidirectionally data-bind the class inclusion of 'collapsed' to the @presenter.collapsed boolean attribute,
# meaning if @presenter.collapsed changes to true, the CSS class 'collapsed' is included on the element,
# and if it changes to false, the CSS class 'collapsed' is removed from the element.
class_name(:collapsed) <= [@presenter, :collapsed]
class_name(:instant_transition) <= [@presenter, :instant_transition]
header(title, class: 'accordion-section-title') {
onclick do |event|
@presenter.toggle_collapsed
if @presenter.collapsed
notify_listeners(:collapsed)
else
notify_listeners(:expanded)
end
end
}
div(slot: :section_content, class: 'accordion-section-content')
}
}
style {
r('.accordion-section-title') {
font_size 2.em
font_weight :bold
cursor :pointer
padding_left 20
position :relative
margin_block_start 0.33.em
margin_block_end 0.33.em
}
r('.accordion-section-title::before') {
content '"▼"'
position :absolute
font_size 0.5.em
top 10
left 0
}
r('.accordion-section-content') {
height 246
overflow :hidden
transition 'height 0.5s linear'
}
r("#{component_element_selector}.instant_transition .accordion-section-content") {
transition 'initial'
}
r("#{component_element_selector}.collapsed .accordion-section-title::before") {
content '"►"'
}
r("#{component_element_selector}.collapsed .accordion-section-content") {
height 0
}
}
end
class Accordion
include Glimmer::Web::Component
events :accordion_section_expanded, :accordion_section_collapsed
markup {
# given that no slots are specified, nesting content under the accordion component
# in consumer code adds content directly inside the markup root div.
div { |accordion| # represents the :markup_root_slot (top-level element)
# on render, all accordion sections would have been added by consumers already, so we can
# attach listeners to all of them by re-opening their content with `.content { ... }` block
on_render do
accordion_section_elements = accordion.children
accordion_sections = accordion_section_elements.map(&:component)
accordion_sections.each_with_index do |accordion_section, index|
accordion_section_number = index + 1
# ensure only the first section is expanded
accordion_section.presenter.collapse(instant: true) if accordion_section_number != 1
accordion_section.content { # re-open content and add component custom event listeners
on_expanded do
other_accordion_sections = accordion_sections.reject {|other_accordion_section| other_accordion_section == accordion_section }
other_accordion_sections.each { |other_accordion_section| other_accordion_section.presenter.collapse }
notify_listeners(:accordion_section_expanded, accordion_section_number)
end
on_collapsed do
notify_listeners(:accordion_section_collapsed, accordion_section_number)
end
}
end
end
}
}
end
# HelloComponentListenersDefaultSlot Glimmer Web Component (View component)
#
# This View component represents the main page being rendered,
# as done by its `render` class method below
#
# Note: this is a simpler version of HelloComponentSlots as it leverages the default slot feature
class HelloComponentListenersDefaultSlot
class Presenter
attr_accessor :status_message
def initialize
@status_message = "Accordion section 1 is expanded!"
end
end
include Glimmer::Web::Component
before_render do
@presenter = Presenter.new
@shipping_address = Address.new(
full_name: 'Johnny Doe',
street: '3922 Park Ave',
street2: 'PO BOX 8382',
city: 'San Diego',
state: 'California',
zip_code: '91913',
)
@billing_address = Address.new(
full_name: 'John C Doe',
street: '123 Main St',
street2: 'Apartment 3C',
city: 'San Diego',
state: 'California',
zip_code: '91911',
)
@emergency_address = Address.new(
full_name: 'Mary Doe',
street: '2038 Ipswitch St',
street2: 'Suite 300',
city: 'San Diego',
state: 'California',
zip_code: '91912',
)
end
markup {
div {
h1(style: {font_style: :italic}) {
inner_html <= [@presenter, :status_message]
}
accordion {
# any content nested under component directly is added to its markup_root_slot element if no default_slot is specified
accordion_section2(title: 'Shipping Address') {
address_form(address: @shipping_address) # automatically inserts content in default_slot :section_content
}
accordion_section2(title: 'Billing Address') {
address_form(address: @billing_address) # automatically inserts content in default_slot :section_content
}
accordion_section2(title: 'Emergency Address') {
address_form(address: @emergency_address) # automatically inserts content in default_slot :section_content
}
# on_accordion_section_expanded listener matches event :accordion_section_expanded declared in Accordion component
on_accordion_section_expanded { |accordion_section_number|
@presenter.status_message = "Accordion section #{accordion_section_number} is expanded!"
}
on_accordion_section_collapsed { |accordion_section_number|
@presenter.status_message = "Accordion section #{accordion_section_number} is collapsed!"
}
}
}
}
end
Document.ready? do
# renders a top-level (root) HelloComponentListenersDefaultSlot component
# Note: this is a simpler version of hello_component_slots.rb as it leverages the default slot feature
HelloComponentListenersDefaultSlot.render
end

No comments: