Using Opal Ruby with Rails 7
Tags: opal, ruby, rails, jquery
Published: 2022-01-13 01:29:55

[Opal](https://github.com/opal/opal) enables writing web front-end code in [Ruby](https://www.ruby-lang.org/), 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](https://github.com/opal/opal) on [Rails](https://rubyonrails.org/) 7.

# Basic Opal Rails 7 Example

[Rails](https://rubyonrails.org/) 7 recently came out with simplified defaults, including defaulting back to Sprockets.

Setting up [Opal](https://github.com/opal/opal) on [Rails](https://rubyonrails.org/) 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:

```ruby
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"`:

```ruby
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:

<h2>Hello World!</h2>

Also, you should see `hello world!` in the browser console.

# Advanced Opal Rails 7 Example

Next, let's build a complete [Rails](https://rubyonrails.org/) application using [Opal Ruby](https://github.com/opal/opal) 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](https://en.wikipedia.org/wiki/WYSIWYG) form. 

![Baseball Cards](https://blogger.googleusercontent.com/img/a/AVvXsEieEeKAiud6COVyGWTpwAzk9OJKK54sJA17vMlom5Tc3tS3wrSMG3jML8PR_FeD-dtLmbxZJi6dkfeOAQCerrKQsSZaZn5U2K_xVUNjQPrLMoE8zTB8RTfmbJC7pcMvh5ZLJYiwgNZTug--nPAWIauJv4hyagwZjzPwC3-uS6AD9eQtkCKYaQ=s16000)

The form looks up random player animated gifs on [Giphy](https://giphy.com/). 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](https://blogger.googleusercontent.com/img/a/AVvXsEhEC1Aa6jXHIWkujM6pA9IzcEbTJinkzgG3red6vNQ4WQBw77Hb0ICzuMmUWKmOf08ojhvSnOywm3ZbUOWQpbT-q9wKkGpW7fPFo267y4MrY7_qlRhk49gZE0Ebp8jE_2QtKWvDnW94GRsbvlxPTOEHCRQhMhG0N3pCt6YO8pi_5ZuR6PDxhw=s16000)

Normally, JavaScript must be involved to interactively build the Baseball Card, but thanks to [Opal](https://github.com/opal/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](https://github.com/opal/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`:

```ruby
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`

```ruby
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`

```ruby
Rails.application.routes.draw do
  resources :baseball_cards
  root "baseball_cards#index"
end
```

`app/models/baseball_card.rb`

```ruby
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`

```ruby
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`

```ruby
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`

```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`

```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`

```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`

```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`

```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
```

![Baseball Cards](https://blogger.googleusercontent.com/img/a/AVvXsEieEeKAiud6COVyGWTpwAzk9OJKK54sJA17vMlom5Tc3tS3wrSMG3jML8PR_FeD-dtLmbxZJi6dkfeOAQCerrKQsSZaZn5U2K_xVUNjQPrLMoE8zTB8RTfmbJC7pcMvh5ZLJYiwgNZTug--nPAWIauJv4hyagwZjzPwC3-uS6AD9eQtkCKYaQ=s16000)

# Opal jQuery Example

Next, let's refactor the code to utilize [Opal jQuery](https://github.com/opal/opal-jquery) in Ruby instead of plain [Opal](https://github.com/opal/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:

```ruby
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](https://github.com/opal/opal-jquery)!

![Baseball Cards](https://blogger.googleusercontent.com/img/a/AVvXsEieEeKAiud6COVyGWTpwAzk9OJKK54sJA17vMlom5Tc3tS3wrSMG3jML8PR_FeD-dtLmbxZJi6dkfeOAQCerrKQsSZaZn5U2K_xVUNjQPrLMoE8zTB8RTfmbJC7pcMvh5ZLJYiwgNZTug--nPAWIauJv4hyagwZjzPwC3-uS6AD9eQtkCKYaQ=s16000)

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!