Last active
October 20, 2024 01:14
-
-
Save AndyObtiva/a6b34e43e08a83e508d460f1b9457a56 to your computer and use it in GitHub Desktop.
Glimmer DSL for Web (Ruby in the Browser Web Frontend Framework) Hello, Component Listeners! Sample
This file contains hidden or 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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment