Andy Maleh
Posted on January 13, 2022
Opal enables writing web front-end code in Ruby, thus producing highly maintainable, productive, and understandable code on both the client-side and server-side.
Below, I present 3 different examples of using Opal Ruby on Rails 7.
Basic Opal Rails 7 Example
Rails 7 recently came out with simplified defaults, including defaulting back to Sprockets.
Setting up Opal on Rails 7 is a breeze as a result.
You may follow these instructions to get a Hello, World! Opal example running in Rails 7:
Opal Rails 7 Example
1- Generate a new Rails app with:
rails new rails_app
2- In your Gemfile
, add:
gem 'opal-rails'
3-
Run opal:install
Rails generator to add app/assets/javascript
to your asset-pipeline manifest in app/assets/config/manifest.js
:
bin/rails g opal:install
4- Delete app/javascript/application.js
5- Enable the following lines in the generated app/assets/javascript/application.js.rb
after require "opal"
:
puts 'hello world!'
require 'native'
$$.document.addEventListener(:DOMContentLoaded) do
$$.document.body.innerHTML = '<h2>Hello World!</h2>'
end
6- Run rails g scaffold welcome
7- Run rails db:migrate
8- Clear app/views/welcomes/index.html.erb
(empty its content)
9- Run rails s
10- Visit http://localhost:3000/welcomes
In the browser webpage, you should see:
Hello World!
Also, you should see hello world!
in the browser console.
Advanced Opal Rails 7 Example
Next, let's build a complete Rails application using Opal Ruby instead of JavaScript, called Baseball Cards!
It will be an animated baseball card creation application that simply takes a player name, team, and position, and renders a baseball card live while information is typed into a WYSIWYG form.
The form looks up random player animated gifs on Giphy. If you do not like the randomly selected photo, you can click the "Another Player Image" button to change it. Otherwise, the form also adds an image for the selected baseball team logo and it edits an SVG element live that represents the player position (e.g. if the player is a 1st-base position player, that part of the SVG lights up yellow). Here is how the "New baseball card" form looks like:
Normally, JavaScript must be involved to interactively build the Baseball Card, but thanks to Opal, we can write most of the code in pure Ruby instead. Note that some Opal Native code was mixed in as well (that is using ticks to execute small bits of JS inside the [Ruby](https://www.ruby-lang.org/) code just like when you use ticks
in CRuby to shell out into the command line terminal), thus demoing this Opal capability too.
The code solution is included below (note that since it is just a demo, I mostly embedded CSS in the elements in the _baseball_card.html.erb
partial).
Opal Rails 7 Advanced Example (Baseball Cards)
1- Run:
rails new baseball_cards
2- Run:
rails g scaffold baseball_cards name:string team:string position:string
3- Run:
rails g migration add_image_url_to_baseball_cards image_url:string
4- Run:
rails db:migrate
5- In your Gemfile, add the following and bundle
:
gem 'opal-rails'
6- Run:
bin/rails g opal:install
7- Delete app/javascript/application.js
8- Replace the content of the following files with the following code:
app/assets/javascript/application.js.rb
require 'opal'
require 'native'
require 'json'
card_image_updater = proc do
name_input = $$.document.getElementById('baseball_card_name')
unless name_input.value.empty?
url = "http://api.giphy.com/v1/gifs/search?q=#{name_input.value}&limit=20&api_key=fM6ptBz7qPw79xrXOagWvHiPzRBSQK7f"
xhttp = Native(`new XMLHttpRequest`)
xhttp.onload = proc do |response|
if `this.readyState` == 4 && `this.status` == 200
response_hash = JSON.parse(`this.responseText`)
image_url = response_hash['data'].sample['url']
image_url = "https://media1.giphy.com/media/#{image_url.split('-').last}/giphy.gif"
card_element = $$.document.querySelectorAll('.card')[0]
card_element.style['background-image'] = "url(#{image_url})"
hidden_image_url_field = $$.document.getElementById('baseball_card_image_url')
hidden_image_url_field.value = image_url
end
end
xhttp.open('GET', url, true)
xhttp.send
end
end
$$.document.addEventListener(:DOMContentLoaded) do
name_input = $$.document.getElementById('baseball_card_name')
name_input&.addEventListener(:change) do
card_name = $$.document.getElementById('card_name')
card_name.innerHTML = name_input.value
card_image_updater.call
end
team_select = $$.document.getElementById('baseball_card_team')
team_select&.addEventListener(:change) do
card_team_image = $$.document.getElementById('card_team')
card_team_value = team_select.value.downcase.gsub(' ', '-')
card_team_value = 'redsox' if card_team_value == 'red-sox' # special case for the red sox
image_url = "https://sportslogosvg.com/wp-content/uploads/2020/09/#{card_team_value}-1200x864.png"
card_team_image.style['display'] = 'inline-block'
card_team_image.src = image_url
end
position_select = $$.document.getElementById('baseball_card_position')
position_select&.addEventListener(:change) do
card_position_image = $$.document.getElementById('card_position')
card_position_image.style['display'] = 'inline-block'
svg_element_id = "text-#{position_select.value.downcase.gsub(' ', '-')}"
$$.document.querySelectorAll('svg text').to_a.each { |text| text.style['fill'] = 'transparent'}
$$.document.getElementById(svg_element_id).style['fill'] = 'yellow'
end
update_card_player_image_button = $$.document.getElementById('update_card_player_image')
update_card_player_image_button&.addEventListener(:click) do |event|
Native(event).preventDefault
card_image_updater.call
end
end
config/routes.rb
Rails.application.routes.draw do
resources :baseball_cards
root "baseball_cards#index"
end
app/models/baseball_card.rb
class BaseballCard < ApplicationRecord
TEAMS = [
{town: 'Chicago', team: 'White Sox'},
{town: 'Cleveland', team: 'Guardians'},
{town: 'Detroit', team: 'Tigers'},
{town: 'Kansas City', team: 'Royals'},
{town: 'Minnesota', team: 'Twins'},
{town: 'Baltimore', team: 'Orioles'},
{town: 'Boston', team: 'Red Sox'},
{town: 'New York', team: 'Yankees'},
{town: 'Tampa Bay', team: 'Rays'},
{town: 'Toronto', team: 'Blue Jays'},
{town: 'Houston', team: 'Astros'},
{town: 'Los Angeles', team: 'Angels'},
{town: 'Oakland', team: 'Athletics'},
{town: 'Seattle', team: 'Mariners'},
{town: 'Texas', team: 'Rangers'},
{town: 'Chicago', team: 'Cubs'},
{town: 'Cincinnati', team: 'Reds'},
{town: 'Milwaukee', team: 'Brewers'},
{town: 'Pittsburgh', team: 'Pirates'},
{town: 'St. Louis', team: 'Cardinals'},
{town: 'Atlanta', team: 'Braves'},
{town: 'Miami', team: 'Marlins'},
{town: 'New York', team: 'Mets'},
{town: 'Philadelphia', team: 'Phillies'},
{town: 'Washington', team: 'Nationals'},
{town: 'Arizona', team: 'Diamondbacks'},
{town: 'Colorado', team: 'Rockies'},
{town: 'Los Angeles', team: 'Dodgers'},
{town: 'San Diego', team: 'Padres'},
{town: 'San Francisco', team: 'Giants'},
]
POSITIONS = [
'Pitcher',
'Catcher',
'1st Base',
'2nd Base',
'3rd Base',
'Shortstop',
'Left Field',
'Center Field',
'Right Field',
]
validates :name, presence: true
validates :image_url, presence: true
end
app/controllers/baseball_cards_controller.rb
class BaseballCardsController < ApplicationController
before_action :set_baseball_card, only: %i[ show edit update destroy ]
# GET /baseball_cards or /baseball_cards.json
def index
@baseball_cards = BaseballCard.all
end
# GET /baseball_cards/1 or /baseball_cards/1.json
def show
end
# GET /baseball_cards/new
def new
@baseball_card = BaseballCard.new
end
# GET /baseball_cards/1/edit
def edit
end
# POST /baseball_cards or /baseball_cards.json
def create
@baseball_card = BaseballCard.new(baseball_card_params)
respond_to do |format|
if @baseball_card.save
format.html { redirect_to baseball_card_url(@baseball_card), notice: "Baseball card was successfully created." }
format.json { render :show, status: :created, location: @baseball_card }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @baseball_card.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /baseball_cards/1 or /baseball_cards/1.json
def update
respond_to do |format|
if @baseball_card.update(baseball_card_params)
format.html { redirect_to baseball_card_url(@baseball_card), notice: "Baseball card was successfully updated." }
format.json { render :show, status: :ok, location: @baseball_card }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @baseball_card.errors, status: :unprocessable_entity }
end
end
end
# DELETE /baseball_cards/1 or /baseball_cards/1.json
def destroy
@baseball_card.destroy
respond_to do |format|
format.html { redirect_to baseball_cards_url, notice: "Baseball card was successfully destroyed." }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_baseball_card
@baseball_card = BaseballCard.find(params[:id])
end
# Only allow a list of trusted parameters through.
def baseball_card_params
params.require(:baseball_card).permit(:name, :team, :position, :image_url)
end
end
app/helpers/baseball_cards_helper.rb
module BaseballCardsHelper
def team_options_for_select(selected=nil)
teams = BaseballCard::TEAMS.reduce({}) do |hash, town_team_hash|
hash.merge(town_team_hash.values.join(' ') => town_team_hash[:team])
end
options_for_select(teams, selected)
end
end
app/views/baseball_cards/_baseball_card.html.erb
<% baseball_card ||= @baseball_card %>
<div class="card" style="float: left; margin: 10px; position: relative; background-size: cover; width: 200px; height: 300px; background-position-x: center; box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2); background-image: url(<%= baseball_card&.image_url %>);">
<div style="position: absolute; bottom: 0px;">
<img id="card_team" src="<%= "https://sportslogosvg.com/wp-content/uploads/2020/09/#{baseball_card&.team&.downcase == 'red sox' ? 'redsox' : baseball_card&.team&.downcase&.sub(' ', '-')}-1200x864.png" %>" height="30" style="display: <%= baseball_card&.team ? 'inline-block' : 'none' %>; vertical-align: middle;" />
<span id="card_name" style="display: inline-block; vertical-align: middle; text-align: center; color: white; font-size: 16px; text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000;">
<%= baseball_card&.name %>
</span>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://web.resource.org/cc/"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
height="35"
viewBox="0 0 611.73914 511.06744"
id="card_position"
style="display: <%= baseball_card&.position ? 'inline-block' : 'none' %>; vertical-align: middle;"
sodipodi:version="0.32"
inkscape:version="0.45.1"
version="1.0"
sodipodi:docbase="C:\Documents and Settings\Chris\Desktop\baseball"
sodipodi:docname="Baseball C.svg"
inkscape:output_extension="org.inkscape.output.svg.inkscape">
<defs
id="defs4">
<linearGradient
id="linearGradient6183">
<stop
style="stop-color:#7e4317;stop-opacity:1;"
offset="0"
id="stop6185" />
<stop
style="stop-color:#953100;stop-opacity:1;"
offset="1"
id="stop6187" />
</linearGradient>
<linearGradient
id="linearGradient5141">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="1"
id="stop5143" />
<stop
style="stop-color:#ffffff;stop-opacity:0;"
offset="1"
id="stop5145" />
</linearGradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient5141"
id="radialGradient5147"
cx="408.7468"
cy="-181.38609"
fx="408.7468"
fy="-181.38609"
r="306.80814"
gradientTransform="matrix(0.1303747,0.4367551,-1.3559209,0.404753,-20.407009,433.33976)"
gradientUnits="userSpaceOnUse" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient5141"
id="radialGradient5170"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-0.1020632,0.3143125,-0.2847171,-9.2452958e-2,409.38007,231.54454)"
cx="992.91998"
cy="429.55511"
fx="992.91998"
fy="429.55511"
r="306.80814" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient6183"
id="radialGradient6191"
cx="528.15991"
cy="389.72467"
fx="528.15991"
fy="389.72467"
r="306.91226"
gradientTransform="matrix(-0.466682,0.4905325,-0.4878269,-0.46411,806.88847,412.71494)"
gradientUnits="userSpaceOnUse" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient6183"
id="radialGradient13054"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-0.466682,0.4905325,-0.4878269,-0.46411,806.88847,412.71494)"
cx="528.15991"
cy="389.72467"
fx="528.15991"
fy="389.72467"
r="306.91226" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient6183"
id="linearGradient13056"
gradientUnits="userSpaceOnUse"
x1="319.04822"
y1="771.89484"
x2="288.61502"
y2="646.47705" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
gridtolerance="10000"
guidetolerance="10"
objecttolerance="10"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1"
inkscape:cx="287.62199"
inkscape:cy="295.73785"
inkscape:document-units="px"
inkscape:current-layer="layer1"
inkscape:window-width="1024"
inkscape:window-height="721"
inkscape:window-x="-4"
inkscape:window-y="-4" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-74.602823,-339.39469)">
<g
id="g14033">
<g
transform="translate(-4,40)"
id="g13047">
<g
id="g10125">
<path
transform="matrix(2.5051227,0,0,1.1727609,-111.7863,-106.80524)"
sodipodi:open="true"
sodipodi:end="6.2831853"
sodipodi:start="3.1333741"
d="M 76.00412,469.36483 A 122,122 0 1 1 320,468.36218"
sodipodi:ry="122"
sodipodi:rx="122"
sodipodi:cy="468.36218"
sodipodi:cx="198"
id="path5135"
style="fill:url(#radialGradient13054);fill-opacity:1;fill-rule:evenodd;stroke:#010000;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
sodipodi:type="arc" />
<path
transform="matrix(5.8551503,0,0,3.3940826,-1489.0661,-1875.1041)"
d="M 320,772.66144 L 293.88965,727.437 L 267.77931,682.21256 L 320,682.21255 L 372.22069,682.21255 L 346.11034,727.437 L 320,772.66144 z "
inkscape:randomized="0"
inkscape:rounded="0"
inkscape:flatsided="false"
sodipodi:arg2="2.6179939"
sodipodi:arg1="1.5707963"
sodipodi:r2="30.14963"
sodipodi:r1="60.299255"
sodipodi:cy="712.36218"
sodipodi:cx="320"
sodipodi:sides="3"
id="path5133"
style="fill:url(#linearGradient13056);fill-opacity:1;fill-rule:evenodd;stroke:#010000;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
sodipodi:type="star" />
<rect
transform="matrix(0.7006506,0.7135045,-0.7135045,0.7006506,0,0)"
y="86.912979"
x="638.72125"
height="164.22331"
width="164.22331"
id="rect7192"
style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:4.02508163;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
</g>
</g>
<path
transform="matrix(0.5974603,0,0,0.5974603,148.07055,602.81434)"
d="M 470 263.36218 A 83 83 0 1 1 304,263.36218 A 83 83 0 1 1 470 263.36218 z"
sodipodi:ry="83"
sodipodi:rx="83"
sodipodi:cy="263.36218"
sodipodi:cx="387"
id="path13062"
style="fill:#833e11;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:4.05200005;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
sodipodi:type="arc" />
</g>
<text
xml:space="preserve"
style="font-size:56px;font-style:normal;font-weight:normal;fill:<%= baseball_card&.position == '1st Base' ? 'yellow' : 'transparent' %>;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Arial"
x="459.22034"
y="641.42615"
id="text-1st-base"><tspan
sodipodi:role="line"
id="text14046"
x="459.22034"
y="641.42615">1B</tspan></text>
<text
xml:space="preserve"
style="font-size:56px;font-style:normal;font-weight:normal;fill:<%= baseball_card&.position == '2nd Base' ? 'yellow' : 'transparent' %>;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Arial"
x="417.87878"
y="554.88501"
id="text-2nd-base"><tspan
sodipodi:role="line"
id="tspan14056"
x="417.87878"
y="554.88501">2B</tspan></text>
<text
xml:space="preserve"
style="font-size:56px;font-style:normal;font-weight:normal;fill:<%= baseball_card&.position == '3rd Base' ? 'yellow' : 'transparent' %>;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Arial"
x="230.89096"
y="641.27338"
id="text-3rd-base"><tspan
sodipodi:role="line"
id="text14058"
x="230.89096"
y="641.27338">3B</tspan></text>
<text
xml:space="preserve"
style="font-size:56px;font-style:normal;font-weight:normal;fill:<%= baseball_card&.position == 'Shortstop' ? 'yellow' : 'transparent' %>;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Arial"
x="273.08279"
y="554.52954"
id="text-shortstop"><tspan
sodipodi:role="line"
id="tspan14064"
x="273.08279"
y="554.52954">SS</tspan></text>
<text
xml:space="preserve"
style="font-size:56px;font-style:normal;font-weight:normal;fill:<%= baseball_card&.position == 'Right Field' ? 'yellow' : 'transparent' %>;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Arial"
x="562.85089"
y="455.53461"
id="text-right-field"><tspan
sodipodi:role="line"
id="text14066"
x="562.85089"
y="455.53461">RF</tspan></text>
<text
xml:space="preserve"
style="font-size:56px;font-style:normal;font-weight:normal;fill:<%= baseball_card&.position == 'Left Field' ? 'yellow' : 'transparent' %>;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Arial"
x="119.05273"
y="454.13171"
id="text-left-field"><tspan
sodipodi:role="line"
id="text14074"
x="119.05273"
y="454.13171">LF</tspan></text>
<text
xml:space="preserve"
style="font-size:56px;font-style:normal;font-weight:normal;fill:<%= baseball_card&.position == 'Center Field' ? 'yellow' : 'transparent' %>;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Arial"
x="344.05273"
y="394.13171"
id="text-center-field"><tspan
sodipodi:role="line"
id="text14078"
x="344.05273"
y="394.13171">CF</tspan></text>
<text
xml:space="preserve"
style="font-size:56px;font-style:normal;font-weight:normal;fill:<%= baseball_card&.position == 'Catcher' ? 'yellow' : 'transparent' %>;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Arial"
x="360.05273"
y="786.13171"
id="text-catcher"><tspan
sodipodi:role="line"
id="text14082"
x="360.05273"
y="786.13171">C</tspan></text>
<text
xml:space="preserve"
style="font-size:56px;font-style:normal;font-weight:normal;fill:<%= baseball_card&.position == 'Pitcher' ? 'yellow' : 'transparent' %>;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Arial"
x="367.49219"
y="676.40515"
id="text-pitcher"><tspan
sodipodi:role="line"
id="text14086"
x="367.49219"
y="676.40515">P</tspan></text>
<path
sodipodi:type="arc"
style="opacity:0.31111115;fill:#ffff00;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:4.05200005;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="path14108"
sodipodi:cx="281.5"
sodipodi:cy="150.56744"
sodipodi:rx="45.5"
sodipodi:ry="45.5"
d="M 327 150.56744 A 45.5 45.5 0 1 1 236,150.56744 A 45.5 45.5 0 1 1 327 150.56744 z"
transform="translate(98.60282,611.39469)" />
</g>
</svg>
</div>
</div>
app/views/baseball_cards/_form.html.erb
<%= form_with(model: baseball_card) do |form| %>
<% if baseball_card.errors.any? %>
<div style="color: red">
<h2><%= pluralize(baseball_card.errors.count, "error") %> prohibited this baseball_card from being saved:</h2>
<ul>
<% baseball_card.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>
<% end %>
<div>
<%= form.label :name, style: "display: block" %>
<%= form.text_field :name %>
</div>
<div>
<%= button_tag 'Another Player Image', type: '', id: :update_card_player_image %>
</div>
<div>
<%= form.label :team, style: "display: block" %>
<%= form.select :team, team_options_for_select(@baseball_card&.team) %>
</div>
<div>
<%= form.label :position, style: "display: block" %>
<%= form.select :position, BaseballCard::POSITIONS %>
</div>
<div>
<%= form.hidden_field :image_url %>
<%= form.submit %>
</div>
<% end %>
<br />
<%= render @baseball_card %>
app/views/baseball_cards/index.html.erb
<p style="color: green"><%= notice %></p>
<h1>Baseball cards</h1>
<%= link_to "New baseball card", new_baseball_card_path %>
<div id="baseball_cards">
<% @baseball_cards.each do |baseball_card| %>
<%= link_to baseball_card do %>
<%= render baseball_card, baseball_card: baseball_card %>
<% end %>
<% end %>
</div>
app/views/baseball_cards/new.html.erb
<h1>New baseball card</h1>
<div>
<%= link_to "Back to baseball cards", baseball_cards_path %>
</div>
<br>
<%= render "form", baseball_card: @baseball_card %>
app/views/baseball_cards/edit.html.erb
<h1>Editing baseball card</h1>
<div>
<%= link_to "Show this baseball card", @baseball_card %> |
<%= link_to "Back to baseball cards", baseball_cards_path %>
</div>
<br>
<%= render "form", baseball_card: @baseball_card %>
9- Run:
rails s
10- Visit:
http://localhost:3000
Opal jQuery Example
Next, let's refactor the code to utilize Opal jQuery in Ruby instead of plain Opal. This simplifies the code in app/assets/javascript/application.js.rb
quite a bit:
1- In your Gemfile, add the following and bundle:
gem 'opal-jquery'
2- Download jquery.js from https://code.jquery.com/jquery-3.6.0.js and save in this location (to be able to later add require 'jquery'
to the Opal code):
app/assets/javascript/jquery.js
3- Replace the content of the file app/assets/javascript/application.js.rb
with the following:
require 'opal'
require 'native'
require 'jquery'
require 'opal-jquery'
card_image_updater = proc do
name_input = Element['#baseball_card_name']
if !name_input.val.empty?
url = "http://api.giphy.com/v1/gifs/search?q=#{name_input.value}&limit=20&api_key=fM6ptBz7qPw79xrXOagWvHiPzRBSQK7f"
HTTP.get(url) do |response|
if response.ok?
response_hash = response.json
image_url = response_hash['data'].sample['url']
image_url = "https://media1.giphy.com/media/#{image_url.split('-').last}/giphy.gif"
card_element = Element['.card']
card_element.css('background-image', "url(#{image_url})")
hidden_image_url_field = Element['#baseball_card_image_url']
hidden_image_url_field.val(image_url)
end
end
end
end
Document.ready? do
name_input = Element['#baseball_card_name']
name_input.on(:change) do
card_name = Element['#card_name']
card_name.html(name_input.value)
card_image_updater.call
end
team_select = Element['#baseball_card_team']
team_select.on(:change) do
card_team_image = Element['#card_team']
card_team_value = team_select.value.downcase.gsub(' ', '-')
card_team_value = 'redsox' if card_team_value == 'red-sox' # special case for the red sox
image_url = "https://sportslogosvg.com/wp-content/uploads/2020/09/#{card_team_value}-1200x864.png"
card_team_image.css('display', 'inline-block')
card_team_image.attr('src', image_url)
end
position_select = Element['#baseball_card_position']
position_select.on(:change) do
card_position_image = Element['#card_position']
card_position_image.css('display', 'inline-block')
svg_element_id = "text-#{position_select.value.downcase.gsub(' ', '-')}"
Element['svg text'].each { |text| text.css('fill', 'transparent')}
Element["##{svg_element_id}"].css('fill', 'yellow')
end
update_card_player_image_button = Element['#update_card_player_image']
update_card_player_image_button.on(:click) do |event|
event.prevent
card_image_updater.call
end
end
4- Run:
rails s
5- Visit:
http://localhost:3000
The same app should continue working, but with more maintainable Opal jQuery Ruby code!
A canonical version of the project is available at GitHub:
https://github.com/AndyObtiva/baseball_cards
Also, hosted on Heroku:
http://animated-baseball-cards.herokuapp.com
Cheers!
Posted on January 13, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.