# [](https://github.com/AndyObtiva/glimmer) Glimmer DSL for Web 0.6.3 (Beta) ## Ruby-in-the-Browser Web Frontend Framework ### The "Rails" of Frontend Frameworks!!! #### Finally, Ruby Developer Productivity, Happiness, and Fun in the Frontend!!! [![Gem Version](https://badge.fury.io/rb/glimmer-dsl-web.svg)](http://badge.fury.io/rb/glimmer-dsl-web) [![Join the chat at https://gitter.im/AndyObtiva/glimmer](https://badges.gitter.im/AndyObtiva/glimmer.svg)](https://gitter.im/AndyObtiva/glimmer?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) **(Based on Original [Glimmer](https://github.com/AndyObtiva/glimmer) Library Handling World’s Ruby GUI Needs Since 2007. Beware of Imitators!)** **(Talk Videos: [Intro to Ruby in the Browser](https://youtu.be/4AdcfbI6A4c?si=MmxOrkhIXTDHQoYi) & [Frontend Ruby with Glimmer DSL for Web](https://youtu.be/rIZ-ILUv9ME?si=raygUXVPd_7ypWuE))** [![Todo MVC](/images/glimmer-dsl-web-samples-regular-todo-mvc.gif)](https://sample-glimmer-dsl-web-rails7-app-black-sound-6793.fly.dev/) You can finally have Ruby developer happiness and productivity in the Frontend! No more wasting time splitting your resources across multiple languages, using badly engineered, over-engineered, or premature-optimization-obsessed JavaScript libraries, fighting JavaScript build issues (e.g. webpack), or rewriting Ruby Backend code in Frontend JavaScript. With [Ruby in the Browser](https://www.youtube.com/watch?v=4AdcfbI6A4c), you can have an exponential jump in development productivity (2x or higher), time-to-release (1/2 or less time), cost (1/2 or cheaper), and maintainability (~50% the code that is simpler and more readable) over JavaScript libraries like React, Angular, Ember, Vue, and Svelte, while being able to reuse Backend Ruby code as is in the Frontend for faster interactions when needed. Also, with Frontend Ruby, companies can cut their hiring budget in half by having Backend Ruby Software Engineers do Frontend Development in Ruby! [Ruby in the Browser](https://www.youtube.com/watch?v=4AdcfbI6A4c) finally fulfills every smart highly-productive Rubyist's dream by bringing Ruby productivity fun to Frontend Development, the same productivity fun you had for years and decades in Backend Development. [Glimmer](https://github.com/AndyObtiva/glimmer) DSL for Web enables building Web Frontends using [Ruby in the Browser](https://www.youtube.com/watch?v=4AdcfbI6A4c), as per [Matz's recommendation in his RubyConf 2022 keynote speech to replace JavaScript with Ruby](https://youtu.be/knutsgHTrfQ?t=789). It supports Rails' principle of the One Person Framework by not requiring any extra developers with JavaScript expertise, yet enabling Ruby (Backend) Software Engineers to develop the Frontend with Ruby code that is better than any JavaScript code produced by JS developers. It aims at providing the simplest, most intuitive, most straight-forward, and most productive frontend framework in existence. The framework follows the Ruby way (with [DSLs](https://martinfowler.com/books/dsl.html) and [TIMTOWTDI](https://en.wiktionary.org/wiki/TMTOWTDI#English)) and the Rails way ([Convention over Configuration](https://rubyonrails.org/doctrine)) in building Isomorphic Ruby on Rails Applications. It provides a Ruby [HTML DSL](#usage) (including full support for [SVG](#hello-svg)), which uniquely enables writing both structure code and logic code in one language. It supports both Unidirectional (One-Way) [Data-Binding](#hello-data-binding) (using `<=`) and Bidirectional (Two-Way) [Data-Binding](#hello-data-binding) (using `<=>`). Dynamic rendering (and re-rendering) of HTML content is also supported via [Content Data-Binding](#hello-content-data-binding). Modular design is supported with [Glimmer Web Components](#hello-component), [Component Slots](#hello-component-slots), and [Component Custom Event Listeners](#hello-component-listeners). And, a Ruby CSS DSL is supported with the included [Glimmer DSL for CSS](https://github.com/AndyObtiva/glimmer-dsl-css). To automatically convert legacy HTML & CSS code to Glimmer DSL Ruby code, Software Engineers could use the included [`html_to_glimmer`](https://github.com/AndyObtiva/glimmer-dsl-xml#html-to-glimmer-converter) and [`css_to_glimmer`](https://github.com/AndyObtiva/glimmer-dsl-css#css-to-glimmer-converter) commands. Many [samples](#samples) are demonstrated in the [Rails sample app](https://github.com/AndyObtiva/sample-glimmer-dsl-web-rails7-app) (there is a very minimal [Standalone [No Rails] static site sample app](https://github.com/Largo/glimmer-dsl-web-standalone-demo) too). You can finally live in pure Rubyland on the Web in both the frontend and backend with [Glimmer DSL for Web](https://rubygems.org/gems/glimmer-dsl-web)! [Glimmer DSL for Web](https://rubygems.org/gems/glimmer-dsl-web) aims to be a very simple Ruby-based drop-in replacement for your existing JavaScript Frontend library (e.g. React, Angular, Vue, Ember, Svelte) or your JavaScript Frontend layer in general. It does not change how your Frontend interacts with the Backend, meaning you can continue to write Rails Backend API endpoints as needed and make HTTP/Ajax requests or read data embedded in elements, but from [Ruby in the Browser](https://www.youtube.com/watch?v=4AdcfbI6A4c). Whatever is possible in JavaScript is possible when using Glimmer DSL for Web as it integrates with any existing JavaScript library. The [Rails sample app](https://github.com/AndyObtiva/sample-glimmer-dsl-web-rails7-app) demonstrates how to [make HTTP calls](https://github.com/AndyObtiva/sample-glimmer-dsl-web-rails7-app/blob/master/app/assets/opal/sample_selector/models/sample_api.rb) and how to [integrate with a JavaScript library](https://github.com/AndyObtiva/sample-glimmer-dsl-web-rails7-app/blob/master/app/views/layouts/application.html.erb) (highlightjs) that performs [code syntax highlighting](https://github.com/AndyObtiva/sample-glimmer-dsl-web-rails7-app/blob/master/app/assets/opal/sample_selector.rb). [Glimmer DSL for Web](https://rubygems.org/gems/glimmer-dsl-web) currently runs on [Opal](https://opalrb.com/) ([Fukuoka Ruby 2023 Award Winner](https://www.digitalfukuoka.jp/topics/228?locale=ja)), a Ruby-to-JavaScript transpiler. In the future, it might support other Frontend Ruby environments, such as [ruby.wasm](https://github.com/ruby/ruby.wasm). After looking through the [samples](#samples) below, read the [FAQ (Frequently Asked Questions)](#faq) to learn more about how Glimmer DSL for Web compares to other approaches/libraries like Hotwire (Turbo), Phlex, ViewComponent, Angular, Vue, React, Svelte, and other JS frameworks. Anyone not considering this kind of technology in 2024 is like someone stuck in the dark ages riding horse carriage (e.g. JavaScript developers using frameworks like React) despite flying cars having been invented already and providing exponential jumps in productivity (way more than small linear jumps provided by some JavaScript libraries). Obviously, those who do make this jump will end up winning their work over from customers and beating the competition while delivering the best Frontend value possible to customers. (Attention Software Engineers, Bloggers, and Contributors: Please use Glimmer DSL for Web in web projects, blog about it, and submit a PR with your article, project, and/or open-source-repo added to the README. Also, I give everyone permission to present this project at their local Ruby user group, local Software Engineering meetup, or Software Conferences outside of North America (e.g. Europe). I am willing to present at Software Conferences in North America and Japan (the birthplace of Ruby) only. If you want to have this project presented elsewhere, like in Europe or South America, feel free to prepare and give your own presentations of the project, and if needed, hit me up for help on the [Glimmer Gitter chat](https://gitter.im/AndyObtiva/glimmer?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)) **Hello, World! Sample** (Note: in real app development, we build [Glimmer Web Components](#hello-component), but this sample is just introducing basic building blocks towards building [components](#hello-component)) [lib/glimmer-dsl-web/samples/hello/hello_world.rb](/lib/glimmer-dsl-web/samples/hello/hello_world.rb) Glimmer HTML DSL Ruby code in the frontend: ```ruby require 'glimmer-dsl-web' include Glimmer Document.ready? do div { 'Hello, World!' } end ``` That produces the following under ``: ```html
Hello, World!
``` ![setup is working](/images/glimmer-dsl-web-setup-example-working.png) You can also mount the `div` elsewhere by passing the `parent: parent_css_selector` option (e.g. `div(parent: 'div#app-container') { 'Hello, World!' }`). **Hello, Button!** (Note: in real app development, we build [Glimmer Web Components](#hello-component), but this sample is just introducing basic building blocks towards building [components](#hello-component)) Event listeners can be setup on any element using the same event names used in HTML (e.g. `onclick`) while passing in a standard Ruby block to handle behavior. `$$` gives access to JS global scope from Ruby to invoke functions like `alert`. [lib/glimmer-dsl-web/samples/hello/hello_button.rb](/lib/glimmer-dsl-web/samples/hello/hello_button.rb) Glimmer HTML DSL Ruby code in the frontend: ```ruby require 'glimmer-dsl-web' include Glimmer Document.ready? do div { button('Greet') { onclick do $$.alert('Hello, Button!') end } } end ``` That produces the following under ``: ```html
``` Screenshot: ![Hello, Button!](/images/glimmer-dsl-web-samples-hello-hello-button.gif) **Hello, Form!** (Note: in real app development, we build [Glimmer Web Components](#hello-component), but this sample is just introducing basic building blocks towards building [components](#hello-component)) [Glimmer DSL for Web](https://rubygems.org/gems/glimmer-dsl-web) gives access to all Web Browser built-in features like HTML form validations, input focus, events, and element functions from a very terse and productive Ruby HTML DSL. Also, you can apply CSS styles by including directly in Ruby code as a string, using [Glimmer DSL for CSS](https://github.com/AndyObtiva/glimmer-dsl-css), or managing CSS completely separately using something like [SCSS](https://sass-lang.com/). The CSS techniques could be combined as well, like by managing common reusable CSS styles separately in SCSS, but adding component specific CSS styles in Ruby when it is more convenient. [lib/glimmer-dsl-web/samples/hello/hello_form.rb](/lib/glimmer-dsl-web/samples/hello/hello_form.rb) Glimmer HTML DSL Ruby code in the frontend: ```ruby require 'glimmer-dsl-web' include Glimmer Document.ready? do div { h1('Contact Form') form { div { label('Name: ', for: 'name-field') @name_input = input(type: 'text', id: 'name-field', required: true, autofocus: true) } div { label('Email: ', for: 'email-field') @email_input = input(type: 'email', id: 'email-field', required: true) } div { input(type: 'submit', value: 'Add Contact') { onclick do |event| if ([@name_input, @email_input].all? {|input| input.check_validity }) # re-open table content and add row @table.content { tr { td { @name_input.value } td { @email_input.value } } } @email_input.value = @name_input.value = '' @name_input.focus end end } } } h1('Contacts Table') @table = table { tr { th('Name') th('Email') } tr { td('John Doe') td('johndoe@example.com') } tr { td('Jane Doe') td('janedoe@example.com') } } # CSS Styles style { # CSS can be included as a String as done below, or as Glimmer DSL for CSS syntax (Ruby code) as done in other samples <<~CSS input { margin: 5px; } input[type=submit] { margin: 5px 0; } table { border:1px solid grey; border-spacing: 0; } table tr td, table tr th { padding: 5px; } table tr:nth-child(even) { background: #ccc; } CSS } } end ``` That produces the following under ``: ```html

Contact Form

Contacts Table

Name Email
John Doe johndoe@example.com
Jane Doe janedoe@example.com
``` Screenshot: ![Hello, Form!](/images/glimmer-dsl-web-samples-hello-hello-form.gif) **Hello, Data-Binding!** (Note: in real app development, we build [Glimmer Web Components](#hello-component), but this sample is just introducing basic building blocks towards building [components](#hello-component)) [Glimmer DSL for Web](https://rubygems.org/gems/glimmer-dsl-web) intuitively supports both Unidirectional (One-Way) Data-Binding via the `<=` operator and Bidirectional (Two-Way) Data-Binding via the `<=>` operator, incredibly simplifying how to sync View properties with Model attributes with the simplest code to reason about. [lib/glimmer-dsl-web/samples/hello/hello_data_binding.rb](/lib/glimmer-dsl-web/samples/hello/hello_data_binding.rb) Glimmer HTML DSL Ruby code in the frontend: ```ruby require 'glimmer-dsl-web' Address = Struct.new(: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 @address = Address.new( street: '123 Main St', street2: 'Apartment 3C, 2nd door to the right', city: 'San Diego', state: 'California', zip_code: '91911', billing_and_shipping: true, ) include Glimmer Document.ready? do div { div(style: 'display: grid; grid-auto-columns: 80px 260px;') { |address_div| label('Street: ', for: 'street-field') input(id: 'street-field') { # Bidirectional Data-Binding with <=> ensures input.value and @address.street # automatically stay in sync when either side changes 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') { # Bidirectional Data-Binding with <=> ensures input.value and @address.zip_code # automatically stay in sync when either side changes # on_write option specifies :to_s method to invoke on value before writing to model attribute # to ensure the numeric zip code value is stored as a String value <=> [@address, :zip_code, on_write: :to_s, ] } div(style: 'grid-column: 1 / span 2') { input(id: 'billing-and-shipping-field', type: 'checkbox') { checked <=> [@address, :billing_and_shipping] } label(for: 'billing-and-shipping-field') { 'Use this address for both Billing & Shipping' } } # Programmable CSS using Glimmer DSL for CSS style { # `r` is an alias for `rule`, generating a CSS rule r("#{address_div.selector} *") { margin '5px' } r("#{address_div.selector} input, #{address_div.selector} select") { grid_column '2' } } } div(style: 'margin: 5px') { # Unidirectional Data-Binding is done with <= to ensure @address.summary changes # automatically update div.inner_text # (computed by changes to address attributes, meaning if street changes, # @address.summary is automatically recomputed.) inner_text <= [@address, :summary, computed_by: @address.members + ['state_code'], ] } } end ``` Screenshot: ![Hello, Data-Binding!](/images/glimmer-dsl-web-samples-hello-hello-data-binding.gif) **Hello, Content Data-Binding!** (Note: in real app development, we build [Glimmer Web Components](#hello-component), but this sample is just introducing basic building blocks towards building [components](#hello-component)) If you need to regenerate HTML element content dynamically, you can use Content Data-Binding to effortlessly rebuild HTML elements based on changes in a Model attribute that provides the source data. In this example, we generate multiple address forms based on the number of addresses the user has using `content(@user, :address_count)` (you can add a `computed_by: array_of_attributes` option if you want to re-render content based on changes to multiple attributes like `content(@user, computed_by: [:address_count, :street_count])`, which fires on changes to `address_count` or `street_count`) . [lib/glimmer-dsl-web/samples/hello/hello_content_data_binding.rb](/lib/glimmer-dsl-web/samples/hello/hello_content_data_binding.rb) Glimmer HTML DSL Ruby code in the frontend: ```ruby require 'glimmer-dsl-web' class Address attr_accessor :text attr_reader :name, :street, :city, :state, :zip def name=(value) @name = value update_text end def street=(value) @street = value update_text end def city=(value) @city = value update_text end def state=(value) @state = value update_text end def zip=(value) @zip = value update_text end private def update_text self.text = [name, street, city, state, zip].compact.reject(&:empty?).join(', ') end end class User attr_accessor :addresses attr_reader :address_count def initialize @address_count = 1 @addresses = [] update_addresses end def address_count=(value) value = [[1, value.to_i].max, 3].min @address_count = value update_addresses end private def update_addresses address_count_change = address_count - addresses.size if address_count_change > 0 address_count_change.times { addresses << Address.new } else address_count_change.abs.times { addresses.pop } end end end @user = User.new include Glimmer Document.ready? do div { div { label('Number of addresses: ', for: 'address-count-field') input(id: 'address-count-field', type: 'number', min: 1, max: 3) { value <=> [@user, :address_count] } } div { # Content Data-Binding is used to dynamically (re)generate content of div # based on changes to @user.addresses, replacing older content on every change content(@user, :address_count) do @user.addresses.each do |address| div { div(style: 'display: grid; grid-auto-columns: 80px 280px;') { |address_div| [:name, :street, :city, :state, :zip].each do |attribute| label(attribute.to_s.capitalize, for: "#{attribute}-field") input(id: "#{attribute}-field", type: 'text') { value <=> [address, attribute] } end div(style: 'grid-column: 1 / span 2;') { inner_text <= [address, :text] } style { r(address_div.selector) { margin '10px 0' } r("#{address_div.selector} *") { margin '5px' } r("#{address_div.selector} label") { grid_column '1' } r("#{address_div.selector} input, #{address_div.selector} select") { grid_column '2' } } } } end end } } end ``` Screenshot: ![Hello, Content Data-Binding!](/images/glimmer-dsl-web-samples-hello-hello-content-data-binding.gif) **Hello, Component!** You can define Glimmer web components (View components) to reuse visual concepts to your heart's content, by simply defining a class with `include Glimmer::Web::Component` and encasing the reusable markup inside a `markup {...}` block. Glimmer web components automatically extend the Glimmer HTML DSL with new keywords that match the underscored versions of the component class names (e.g. an `OrderSummary` class yields the `order_summary` keyword for reusing that component within the Glimmer HTML DSL). You may insert a Glimmer component anywhere into a Rails View using `glimmer_component(component_path, *args)` Rails helper (more about it in [Hello, glimmer_component Rails Helper!](#hello-glimmer_component-rails-helper)). Below, we define an `AddressForm` component that generates an `address_form` keyword, and then we reuse it twice inside an `AddressPage` component displaying a Shipping Address and a Billing Address. You can specify CSS styles that apply to all instances of a component by opening a `style {...}` block, which is evaluated against the component class given that it applies to all instances. That would automatically generate one ` ``` Screenshot: ![Hello, Form!](/images/glimmer-dsl-web-samples-hello-hello-form.gif) #### Hello, Form (MVP)! This is the MVP (Model-View-Presenter) edition of Hello, Form! leveraging Glimmer Web Components and the MVP Architectural Pattern. Main file: [lib/glimmer-dsl-web/samples/hello/hello_form_mvp.rb](/lib/glimmer-dsl-web/samples/hello/hello_form_mvp.rb) Other files: [lib/glimmer-dsl-web/samples/hello/hello_form_mvp](/lib/glimmer-dsl-web/samples/hello/hello_form_mvp) Glimmer HTML DSL Ruby code in the frontend: ```ruby require 'glimmer-dsl-web' require_relative 'hello_form_mvp/presenters/hello_form_mvp_presenter' require_relative 'hello_form_mvp/views/contact_form' require_relative 'hello_form_mvp/views/contact_table' class HelloFormMvp include Glimmer::Web::Component before_render do @presenter = HelloFormMvpPresenter.new end markup { div { h1('Contact Form') contact_form(presenter: @presenter) h1('Contacts Table') contact_table(presenter: @presenter) } } end Document.ready? do HelloFormMvp.render end ``` Screenshot: ![Hello, Form!](/images/glimmer-dsl-web-samples-hello-hello-form.gif) #### Hello, Observer! [Glimmer DSL for Web](https://rubygems.org/gems/glimmer-dsl-web) provides the `observe(model, attribute) { ... }` keyword to employ the [Observer Design Pattern](https://en.wikipedia.org/wiki/Observer_pattern) as per [MVC](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) (Model View Controller), enabling Views to observe Models and update themselves in response to changes. If the `observe` keyword is used from inside a [Component](#hello-component), when the Component is removed or its top-level element is removed, the observer is automatically cleaned up. The need for such explicit observers is significantly diminished by the availablility of the more advanced Unidirectional [Data-Binding](#hello-data-binding) Support and Bidirectional [Data-Binding](#hello-data-binding) Support. [lib/glimmer-dsl-web/samples/hello/hello_observer.rb](/lib/glimmer-dsl-web/samples/hello/hello_observer.rb) Glimmer HTML DSL Ruby code in the frontend: ```ruby require 'glimmer-dsl-web' class NumberHolder attr_accessor :number def initialize self.number = 50 end end class HelloObserver include Glimmer::Web::Component before_render do @number_holder = NumberHolder.new end after_render do @number_input.value = @number_holder.number @range_input.value = @number_holder.number # Observe Model attribute @number_holder.number for changes and update View elements. # Observer is automatically cleaned up when `remove` method is called on rendered # HelloObserver web component or its top-level markup element (div) observe(@number_holder, :number) do number_string = @number_holder.number.to_s @number_input.value = number_string unless @number_input.value == number_string @range_input.value = number_string unless @range_input.value == number_string end # Bidirectional Data-Binding does the same thing automatically as per alternative sample: Hello, Observer (Data-Binding)! end markup { div { div { @number_input = input(type: 'number', min: 0, max: 100) { # oninput listener (observer) updates Model attribute @number_holder.number oninput do @number_holder.number = @number_input.value.to_i end } } div { @range_input = input(type: 'range', min: 0, max: 100) { # oninput listener (observer) updates Model attribute @number_holder.number oninput do @number_holder.number = @range_input.value.to_i end } } } } end Document.ready? do HelloObserver.render end ``` Screenshot: ![Hello, Observer!](/images/glimmer-dsl-web-samples-hello-hello-observer.gif) #### Hello, Observer (Data-Binding)! This is the data-binding edition of Hello, Observer!, which uses the `<=>` operator to perform bidirectional data-binding between a View property and a Model attribute, thus yield a lot less code that is declarative and is the most minimal code possible to express the requirements. [lib/glimmer-dsl-web/samples/hello/hello_observer_data_binding.rb](/lib/glimmer-dsl-web/samples/hello/hello_observer_data_binding.rb) Glimmer HTML DSL Ruby code in the frontend: ```ruby require 'glimmer-dsl-web' class NumberHolder attr_accessor :number def initialize self.number = 50 end end class HelloObserver include Glimmer::Web::Component before_render do @number_holder = NumberHolder.new end markup { div { div { input(type: 'number', min: 0, max: 100) { value <=> [@number_holder, :number] } } div { input(type: 'range', min: 0, max: 100) { value <=> [@number_holder, :number] } } } } end Document.ready? do HelloObserver.render end ``` Screenshot: ![Hello, Observer!](/images/glimmer-dsl-web-samples-hello-hello-observer.gif) #### Hello, Data-Binding! [Glimmer DSL for Web](https://rubygems.org/gems/glimmer-dsl-web) intuitively supports both Unidirectional (One-Way) Data-Binding via the `<=` operator and Bidirectional (Two-Way) Data-Binding via the `<=>` operator, incredibly simplifying how to sync View properties with Model attributes with the simplest code to reason about. [lib/glimmer-dsl-web/samples/hello/hello_data_binding.rb](/lib/glimmer-dsl-web/samples/hello/hello_data_binding.rb) Glimmer HTML DSL Ruby code in the frontend: ```ruby require 'glimmer-dsl-web' Address = Struct.new(: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 @address = Address.new( street: '123 Main St', street2: 'Apartment 3C, 2nd door to the right', city: 'San Diego', state: 'California', zip_code: '91911', billing_and_shipping: true, ) include Glimmer Document.ready? do div { div(style: 'display: grid; grid-auto-columns: 80px 260px;') { |address_div| label('Street: ', for: 'street-field') input(id: 'street-field') { # Bidirectional Data-Binding with <=> ensures input.value and @address.street # automatically stay in sync when either side changes 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') { # Bidirectional Data-Binding with <=> ensures input.value and @address.zip_code # automatically stay in sync when either side changes # on_write option specifies :to_s method to invoke on value before writing to model attribute # to ensure the numeric zip code value is stored as a String value <=> [@address, :zip_code, on_write: :to_s, ] } div(style: 'grid-column: 1 / span 2') { input(id: 'billing-and-shipping-field', type: 'checkbox') { checked <=> [@address, :billing_and_shipping] } label(for: 'billing-and-shipping-field') { 'Use this address for both Billing & Shipping' } } # Programmable CSS using Glimmer DSL for CSS style { # `r` is an alias for `rule`, generating a CSS rule r("#{address_div.selector} *") { margin '5px' } r("#{address_div.selector} input, #{address_div.selector} select") { grid_column '2' } } } div(style: 'margin: 5px') { # Unidirectional Data-Binding is done with <= to ensure @address.summary changes # automatically update div.inner_text # (computed by changes to address attributes, meaning if street changes, # @address.summary is automatically recomputed.) inner_text <= [@address, :summary, computed_by: @address.members + ['state_code'], ] } } end ``` Screenshot: ![Hello, Data-Binding!](/images/glimmer-dsl-web-samples-hello-hello-data-binding.gif) #### Hello, Content Data-Binding! If you need to regenerate (re-render) HTML element content dynamically, you can use Content Data-Binding to effortlessly rebuild (rerender) HTML elements based on changes in a Model attribute that provides the source data. In this example, we generate multiple address forms based on the number of addresses the user has using `content(@user, :address_count)` (you can add a `computed_by: array_of_attributes` option if you want to re-render content based on changes to multiple attributes like `content(@user, computed_by: [:address_count, :street_count])`, which fires on changes to `address_count` or `street_count`) . [lib/glimmer-dsl-web/samples/hello/hello_content_data_binding.rb](/lib/glimmer-dsl-web/samples/hello/hello_content_data_binding.rb) Glimmer HTML DSL Ruby code in the frontend: ```ruby require 'glimmer-dsl-web' class Address attr_accessor :text attr_reader :name, :street, :city, :state, :zip def name=(value) @name = value update_text end def street=(value) @street = value update_text end def city=(value) @city = value update_text end def state=(value) @state = value update_text end def zip=(value) @zip = value update_text end private def update_text self.text = [name, street, city, state, zip].compact.reject(&:empty?).join(', ') end end class User attr_accessor :addresses attr_reader :address_count def initialize @address_count = 1 @addresses = [] update_addresses end def address_count=(value) value = [[1, value.to_i].max, 3].min @address_count = value update_addresses end private def update_addresses address_count_change = address_count - addresses.size if address_count_change > 0 address_count_change.times { addresses << Address.new } else address_count_change.abs.times { addresses.pop } end end end @user = User.new include Glimmer Document.ready? do div { div { label('Number of addresses: ', for: 'address-count-field') input(id: 'address-count-field', type: 'number', min: 1, max: 3) { value <=> [@user, :address_count] } } div { # Content Data-Binding is used to dynamically (re)generate content of div # based on changes to @user.address_count, replacing older content on every change content(@user, :address_count) do @user.addresses.each do |address| div { div(style: 'display: grid; grid-auto-columns: 80px 280px;') { |address_div| [:name, :street, :city, :state, :zip].each do |attribute| label(attribute.to_s.capitalize, for: "#{attribute}-field") input(id: "#{attribute}-field", type: 'text') { value <=> [address, attribute] } end div(style: 'grid-column: 1 / span 2;') { inner_text <= [address, :text] } style { r(address_div.selector) { margin '10px 0' } r("#{address_div.selector} *") { margin '5px' } r("#{address_div.selector} label") { grid_column '1' } r("#{address_div.selector} input, #{address_div.selector} select") { grid_column '2' } } } } end end } } end ``` Screenshot: ![Hello, Content Data-Binding!](/images/glimmer-dsl-web-samples-hello-hello-content-data-binding.gif) #### Hello, Component! You can define Glimmer web components (View components) to reuse visual concepts to your heart's content, by simply defining a class with `include Glimmer::Web::Component` and encasing the reusable markup inside a `markup {...}` block. Glimmer web components automatically extend the Glimmer HTML DSL with new keywords that match the underscored versions of the component class names (e.g. an `OrderSummary` class yields the `order_summary` keyword for reusing that component within the Glimmer HTML DSL). You may insert a Glimmer component anywhere into a Rails View using `glimmer_component(component_path, *args)` Rails helper (more about it in [Hello, glimmer_component Rails Helper!](#hello-glimmer_component-rails-helper)). Below, we define an `AddressForm` component that generates an `address_form` keyword, and then we reuse it twice inside an `AddressPage` component displaying a Shipping Address and a Billing Address. You can specify CSS styles that apply to all instances of a component by opening a `style {...}` block, which is evaluated against the component class given that it applies to all instances. That would automatically generate one `