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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:
Post a Comment