DEV Community

Cover image for Using Opal Ruby with Rails 7
Andy Maleh
Andy Maleh

Posted on • Edited on

Using Opal Ruby with Rails 7

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

2- In your Gemfile, add:

gem 'opal-rails' 
Enter fullscreen mode Exit fullscreen mode

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

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

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.

Baseball Cards

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:

New Baseball Card

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

2- Run:

rails g scaffold baseball_cards name:string team:string position:string 
Enter fullscreen mode Exit fullscreen mode

3- Run:

rails g migration add_image_url_to_baseball_cards image_url:string 
Enter fullscreen mode Exit fullscreen mode

4- Run:

rails db:migrate 
Enter fullscreen mode Exit fullscreen mode

5- In your Gemfile, add the following and bundle:

gem 'opal-rails' 
Enter fullscreen mode Exit fullscreen mode

6- Run:

bin/rails g opal:install 
Enter fullscreen mode Exit fullscreen mode

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

config/routes.rb

Rails.application.routes.draw do resources :baseball_cards root "baseball_cards#index" end 
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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

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

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

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

9- Run:

rails s 
Enter fullscreen mode Exit fullscreen mode

10- Visit:

http://localhost:3000 
Enter fullscreen mode Exit fullscreen mode

Baseball Cards

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

4- Run:

rails s 
Enter fullscreen mode Exit fullscreen mode

5- Visit:

http://localhost:3000 
Enter fullscreen mode Exit fullscreen mode

The same app should continue working, but with more maintainable Opal jQuery Ruby code!

Baseball Cards

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!

Top comments (2)

Collapse
 
ben profile image
Ben Halpern

Nice stuff

Collapse
 
xuwupeng2000 profile image
Jack Wu

👍🏻