DEV Community

Cover image for Glimmer DSL for Web Ruby Integration with JavaScript Libraries
Andy Maleh
Andy Maleh

Posted on

Glimmer DSL for Web Ruby Integration with JavaScript Libraries

Glimmer DSL for Web is a Ruby-in-the-Browser 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's Rails sample app "Sample Selector" has been upgraded with Code Syntax Highlighting by integrating with highlight.js. 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

The Sample Selector 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:

<!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>
Enter fullscreen mode Exit fullscreen mode

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):

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

<%= glimmer_component('sample_selector') %>
Enter fullscreen mode Exit fullscreen mode

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:

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
Enter fullscreen mode Exit fullscreen mode

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:

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
Enter fullscreen mode Exit fullscreen mode

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:

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
Enter fullscreen mode Exit fullscreen mode

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

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
Enter fullscreen mode Exit fullscreen mode

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:

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
Enter fullscreen mode Exit fullscreen mode

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:

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
Enter fullscreen mode Exit fullscreen mode

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

# 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
Enter fullscreen mode Exit fullscreen mode

In conclusion, we demonstrated in the Sample Selector 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 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."

Top comments (0)