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.
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>
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') %>
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
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
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
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
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
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
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
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)