Glimmer DSL for Web Ruby Integration with JavaScript Libraries
Tags: ruby, rails, frontend, glimmer
Published: 2024-05-17 21:28:03

[Glimmer DSL for Web](https://github.com/AndyObtiva/glimmer-dsl-web) is a [Ruby-in-the-Browser](https://www.youtube.com/watch?v=4AdcfbI6A4c) Web Frontend Framework that enables Rubyists to finally have Ruby productivity and happiness in the Frontend via a simpler, more intuitive, more straightforward, and more productive library than all JavaScript libraries like React, Angular, Ember, Vue, Svelte, etc.... [Glimmer DSL for Web](https://github.com/AndyObtiva/glimmer-dsl-web)'s Rails sample app "[Sample Selector](https://github.com/AndyObtiva/sample-glimmer-dsl-web-rails7-app)" has been upgraded with Code Syntax Highlighting by integrating with [highlight.js](https://highlightjs.org/). It demonstrates how to integrate with JavaScript libraries, how to build Glimmer Web Components in the Frontend, and how to make HTTP calls from a Ruby Frontend to a Ruby Backend in a Rails application, among other things.

![Sample Selector](https://github.com/AndyObtiva/sample-glimmer-dsl-web-rails7-app/raw/master/sample-glimmer-dsl-web-rails7-app.gif)

The [Sample Selector](https://github.com/AndyObtiva/sample-glimmer-dsl-web-rails7-app) Rails app enables users to list all samples that ship with the glimmer-dsl-web Ruby gem, show each sample's entry point code, and run the sample in the browser. 

Here is a breakdown of the code:

The Rails layout includes links to the highlight.js javascript CDN URLs:

```erb
<!DOCTYPE html>
<html>
  <head>
    <title>SampleGlimmerDslOpalRails7App</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/ruby.min.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/highlightjs-line-numbers.js/2.8.0/highlightjs-line-numbers.min.js"></script>
    <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
    <%= javascript_importmap_tags %>
  </head>

  <body>
    <%= yield %>
  </body>
</html>
```

The Rails View renders a Glimmer Web Component using the `glimmer_component` Rails helper (this is meant to be a drop-in replacement for JS solutions like `react_component`):

```erb
<h1 style="text-align: center;">Glimmer DSL for Web Rails 7 Sample App</h1>

<%= glimmer_component('sample_selector') %>
```

The Sample Selector Top-Level Glimmer Web Component is the entry point to the Frontend application, which simply consists of 2 other Glimmer Web Components, the Samples Table (samples_table) and the Highlighted Code (highlighted_code). It is as simple as the code could get, and way simpler and lighter than alternative JavaScript library approaches like React, thus cutting down on maintainability cost and time to delivery significantly, for both small and large apps alike:

```ruby
require 'glimmer-dsl-web'

require_relative 'sample_selector/presenters/sample_selector_presenter'
require_relative 'sample_selector/views/back_anchor'
require_relative 'sample_selector/views/highlighted_code'
require_relative 'sample_selector/views/samples_table'

class SampleSelector
  include Glimmer::Web::Component
  
  before_render do
    @presenter = SampleSelectorPresenter.new
  end
  
  markup {
    div {
      h2("Run a sample or view a sample's code.", style: 'text-align: center;')
      
      highlighted_code(language: 'ruby', model: @presenter, model_code_attribute: :selected_sample_code, float: 'right')
      
      samples_table(presenter: @presenter)
      
      button('Run', style: 'margin: 15px 0; padding: 10px; font-size: 1.3em;') {
        onclick do |event|
          event.prevent_default
          markup_root.remove
          BackAnchor.render
          @presenter.run
        end
      }
    }
  }
end
```

The Highlighted Code Glimmer Web Component declaratively handles loading sample code into an HTML View innerHTML through unidirectional data-binding to the Sample Model code attribute, applying syntax highlighting using highlight.js after reading from the Model whenever updates happen to the View. The code has an excellent separation of concerns between the View and Model, and is a lot simpler than equivalent React code, thus much easier to reason about. Additionally, some CSS was written directly in a Ruby DSL:

```ruby
class HighlightedCode
  include Glimmer::Web::Component
  
  attribute :language, default: 'ruby'
  attribute :model
  attribute :model_code_attribute # name of attribute on model that holds code string to render
  attribute :float, default: 'initial'
  
  markup {
    div(class: 'code-scrollable-container', style: "float: #{float};") {
      pre {
        @code_element = code(class: "language-#{language}") {
          inner_html <= [model, model_code_attribute,
                          after_read: -> (_) { highlight_code }
                        ]
        }
      }
     
      style {
        r('div.code-scrollable-container') {
          overflow 'scroll'
          width 'calc(100vw - 410px)'
          height '80vh'
          border '1px solid rgb(209, 215, 222)'
          padding '0'
        }
        
        r('div.code-scrollable-container pre') {
          margin '0'
        }
        
        r('.hljs-ln td.hljs-ln-line.hljs-ln-numbers') {
          user_select 'none'
          text_align 'center'
          color 'rgb(101, 109, 118)'
          padding '3px 30px'
        }
      }
    }
  }
  
  def highlight_code
    @code_element.dom_element.removeAttr('data-highlighted')
    # $$ enables interacting with global JS scope, like top-level hljs variable to use highlight.js library
    $$.hljs.highlightAll
    $$.hljs.initLineNumbersOnLoad
  end
end
```

The Samples Table lists all the samples that are included in the `glimmer-dsl-web` Ruby gem, and provides the ability to select a sample and run it. It also interacts with a Sample API Web Service to pull sample code data from the Rails backend, thus ensuring a proper separation of concerns for maximum maintainability. Some CSS is added with a Ruby DSL as well:

```ruby
class SamplesTable
  include Glimmer::Web::Component
  
  attribute :presenter
  
  markup {
    table(class: 'samples') {
      tbody {
        SampleSelectorPresenter::SAMPLES.each do |sample|
          tr {
            td(sample.name)
            
            class_name <= [presenter, :selected_sample,
                            on_read: -> (val) { val == sample ? 'selected' : ''}
                          ]
          
            onclick do |event|
              event.prevent_default
              presenter.selected_sample = sample
            end
          }
        end
        
        tr {
          # handle this sample differently via links to demonstrate visiting outside pages
          td {
            span('Hello, glimmer_component Rails Helper!')
            span(' ( ')
            a('Run', "data-turbo": "false", href: '/?address=true&full_name=John%20Doe&street=123%20Main%20St&street2=apt%2012&city=San%20Diego&state=California&zip_code=91911')
            span(' | ')
            a('Code',
              target: '_blank',
              href: 'https://github.com/AndyObtiva/sample-glimmer-dsl-web-rails7-app/blob/master/app/views/welcomes/_address_page.html.erb'
            )
            span(' ) ')
          }
        }
      }
        
      style {
        r('table.samples') {
          border_spacing 0
        }
        
        r('table.samples tr td') {
          border '1px solid transparent'
          padding '5px'
        }
        
        r('table.samples tr td:hover') {
          border '1px solid gray'
        }
        
        r('table.samples tr.selected td') {
          border '1px solid lightgray'
          background 'lightblue'
        }
      }
    }
  }
end
```

The Back Anchor provides a back-link to enable going back to the main page from any run sample:

```ruby
require 'glimmer-dsl-web'

class BackAnchor
  include Glimmer::Web::Component
  
  markup {
    a('<< Back To Samples', href: '#', style: 'display: block; margin-bottom: 10px;') { |back_anchor|
      onclick do
        back_anchor.remove
        Element['body > *'].to_a[2..].each(&:remove)
        SampleSelector.render(parent: ".sample_selector")
      end
    }
  }
end
```

The Sample Selector Presenter provides all the data and actions that the Sample Selector View needs, including how to present sample names, the attribute for storing the selected sample, and the method for running a sample, which delegates to the Sample Model to keep concerns better separated for higher maintainability:

```ruby
require_relative '../models/sample'

class SampleSelectorPresenter
  SAMPLE_NAMES = {
    'hello_world' => 'Hello, World!',
    'hello_button' => 'Hello, Button!',
    'hello_form' => 'Hello, Form!',
    'hello_observer' => 'Hello, Observer!',
    'hello_observer_data_binding' => 'Hello, Observer (Data-Binding)!',
    'hello_data_binding' => 'Hello, Data-Binding!',
    'hello_content_data_binding' => 'Hello, Content Data-Binding!',
    'hello_component' => 'Hello, Component!',
    'hello_paragraph' => 'Hello, Paragraph!',
    'hello_input_date_time' => 'Hello, Input Date/Time!',
    'hello_form_mvp' => 'Hello, Form (MVP)!',
    'button_counter' => 'Button Counter',
  }
  
  SAMPLES = SAMPLE_NAMES.map { |id, name| Sample.new(id:, name:) }
  
  attr_reader :selected_sample
  attr_accessor :selected_sample_code
  
  def initialize
    self.selected_sample = SAMPLES.first
  end
  
  def selected_sample=(sample)
    # causes selected sample to get highlighted in the View indirectly through data-binding
    @selected_sample = sample
    
    @selected_sample.fetch_code do |code|
      # causes selected sample code to display in the View indirectly through data-binding
      self.selected_sample_code = code
    end
  end
  
  def run
    selected_sample.run
  end
end
```

The Sample Model stores sample specific data like the code and ID, enables fetching sample code from the Rails Backend using a Sample API Web Service, and provides and the ability to run a sample by loading the associated sample Ruby file:

```ruby
require_relative 'sample_api'

# Sample Frontend Model
class Sample
  attr_reader :id, :name, :code
  
  def initialize(id:, name:)
    @id = id
    @name = name
  end
  
  def fetch_code(&code_processor)
    SampleApi.show(id) do |sample|
      @code = sample.code
      code_processor.call(@code)
    end
  end
  
  def run
    # We must embeded static require/load statements for Opal to pre-load them into page
    case @id
    when 'hello_world'
      begin
        load 'glimmer-dsl-web/samples/hello/hello_world.rb'
      rescue LoadError # the first time a file is loaded, it raises LoadError and must be required instead
        require 'glimmer-dsl-web/samples/hello/hello_world.rb'
      end
    when 'hello_button'
      begin
        load 'glimmer-dsl-web/samples/hello/hello_button.rb'
      rescue LoadError # the first time a file is loaded, it raises LoadError and must be required instead
        require 'glimmer-dsl-web/samples/hello/hello_button.rb'
      end
    when 'hello_form'
      begin
        load 'glimmer-dsl-web/samples/hello/hello_form.rb'
      rescue LoadError # the first time a file is loaded, it raises LoadError and must be required instead
        require 'glimmer-dsl-web/samples/hello/hello_form.rb'
      end
    when 'hello_observer'
      begin
        load 'glimmer-dsl-web/samples/hello/hello_observer.rb'
      rescue LoadError # the first time a file is loaded, it raises LoadError and must be required instead
        require 'glimmer-dsl-web/samples/hello/hello_observer.rb'
      end
    when 'hello_observer_data_binding'
      begin
        load 'glimmer-dsl-web/samples/hello/hello_observer_data_binding.rb'
      rescue LoadError # the first time a file is loaded, it raises LoadError and must be required instead
        require 'glimmer-dsl-web/samples/hello/hello_observer_data_binding.rb'
      end
    when 'hello_data_binding'
      begin
        load 'glimmer-dsl-web/samples/hello/hello_data_binding.rb'
      rescue LoadError # the first time a file is loaded, it raises LoadError and must be required instead
        require 'glimmer-dsl-web/samples/hello/hello_data_binding.rb'
      end
    when 'hello_content_data_binding'
      begin
        load 'glimmer-dsl-web/samples/hello/hello_content_data_binding.rb'
      rescue LoadError # the first time a file is loaded, it raises LoadError and must be required instead
        require 'glimmer-dsl-web/samples/hello/hello_content_data_binding.rb'
      end
    when 'hello_component'
      begin
        load 'glimmer-dsl-web/samples/hello/hello_component.rb'
      rescue LoadError # the first time a file is loaded, it raises LoadError and must be required instead
        require 'glimmer-dsl-web/samples/hello/hello_component.rb'
      end
    when 'hello_paragraph'
      begin
        load 'glimmer-dsl-web/samples/hello/hello_paragraph.rb'
      rescue LoadError # the first time a file is loaded, it raises LoadError and must be required instead
        require 'glimmer-dsl-web/samples/hello/hello_paragraph.rb'
      end
    when 'hello_input_date_time'
      begin
        load 'glimmer-dsl-web/samples/hello/hello_input_date_time.rb'
      rescue LoadError # the first time a file is loaded, it raises LoadError and must be required instead
        require 'glimmer-dsl-web/samples/hello/hello_input_date_time.rb'
      end
    when 'hello_form_mvp'
      begin
        load 'glimmer-dsl-web/samples/hello/hello_form_mvp.rb'
      rescue LoadError # the first time a file is loaded, it raises LoadError and must be required instead
        require 'glimmer-dsl-web/samples/hello/hello_form_mvp.rb'
      end
    when 'button_counter'
      begin
        load 'glimmer-dsl-web/samples/regular/button_counter.rb'
      rescue LoadError # the first time a file is loaded, it raises LoadError and must be required instead
        require 'glimmer-dsl-web/samples/regular/button_counter.rb'
      end
    end
  end
end
```

The Sample API Web Service simply makes HTTP calls to the Rails backend to grab data:

```ruby
# Sample Resource HTTP API that calls Rails Backend Samples API Endpoint
class SampleApi
  class << self
    def show(id, &sample_processor)
      HTTP.get("/samples/#{id}.json") do |response|
        # Wrapping response body with Native to convert from a JS object to an Opal Ruby object, 
        # thus converting JS properties into Ruby methods that facilitate interaction with object.
        sample = Native(response.body)
        sample_processor.call(sample)
      end
    end
  end
end
```

In conclusion, we demonstrated in the [Sample Selector](https://github.com/AndyObtiva/sample-glimmer-dsl-web-rails7-app) app a Rails Frontend application that is written fully in Ruby with Ruby DSLs in place of HTML/CSS/JavaScript, is following the Model-View-Presenter pattern (a variation on MVC), includes HTTP calls to a Rails Backend to demonstrate real Business Use-Cases of Frontend/Backend interaction, and integrates with an external JavaScript library (highlight.js) for Code Syntax Highlighting. 

As a result, Rails Software Engineers do not have to be split anymore between those who are afraid of Frontend Development because of JavaScript, and those who prefer to do Frontend Development in spite of JavaScript's pitfalls. Instead, there is a 3rd better way that unites all Rails Software Engineers! Just write the Frontend in the same language you love so much in the Backend due to offering maximum productivity and maintainability: Ruby! This should become the no-brainer future of all Frontend development in Rails!!!

I will leave you with the [Glimmer DSL for Web](https://github.com/AndyObtiva/glimmer-dsl-web) introduction from the project README on GitHub:

"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, 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. Ruby in the Browser finally fulfills every 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."