Showing posts with label forms. Show all posts
Showing posts with label forms. Show all posts

Tuesday, 23 December 2025

Web Tutorial: Ruby On Rails Xmas Poll (Part 3/4)

We have a form, and now it's a matter of submitting it. In the Submit action of the Poll controller, we want to collect this data and send it to Oracle APEX.

We'll have to first massage this data into a payload to send. For that, we declare answers as the the collection of all the HTML elements with answers as the name. Then we declare payload as an object with one property, answers. The value of that, will be answers.

app/controllers/poll_controller.rb
def submit
    answers = params[:answers]
    payload = { answers: answers }

end


We then use HTTParty to POST, just like we used it to send a GET request earlier. The base URL is the same - we'll use ORDS_API_URL. For the body, we use payload after running the to_json() method on it, to convert it to a JSON object. And because of this, we should specify that it's JSON in the headers object. The result is returned in the variable response.

app/controllers/poll_controller.rb
def submit
    answers = params[:answers]
    payload = { answers: answers }

    response = HTTParty.post(
        ORDS_API_URL,

        body: payload.to_json,
        headers: {
            "Content-Type" => "application/json"
        }
    )
end


Now, if it's successful, the code property of response will be 200. In that case flash a green success message. If not, flash a red error message.

app/controllers/poll_controller.rb
def submit
    answers = params[:answers]
    payload = { answers: answers }

    response = HTTParty.post(
        ORDS_API_URL,
        body: payload.to_json,
        headers: {
            "Content-Type" => "application/json"
        }
    )

    if response.code == 200
        flash[:notice] = "Submission successful!"

    else
        flash[:alert] = "API error."

    end
end


When all's said and done, use the redirect_to statement to return to root_path, which is the poll form.
app/controllers/poll_controller.rb
def submit
    answers = params[:answers]
    payload = { answers: answers }

    response = HTTParty.post(
        ORDS_API_URL,
        body: payload.to_json,
        headers: {
            "Content-Type" => "application/json"
        }
    )

    if response.code == 200
        flash[:notice] = "Submission successful!"
    else
        flash[:alert] = "API error."
    end

    redirect_to root_path
end


Next, let's go back to Oracle APEX. Remember we created the GET handler for the API endpoint "poll/:id"? Well, now create a POST handler.


Make sure the id variable is defined, and we tell Oracle APEX that it's to be found in the URL.



Before we examine the PL/SQL code for the handler, this is the shape of the data that will be sent, as an example.
{
  "Answers": {
    "1": "4",
    "2": "5",
    "3": "1",
    "4": "2",
    "5": "1",
    "6": "4",
    "7": "4",
    "8": "3",
    "9": "3",
    "10": "5"
  }
}


We have a DECLARE, BEGIN and END statements. After DECLARE, we declare l_request_body_clob as a CLOB object. A CLOB is a Character Large Object, which pretty much describes the data that will be sent to Oracle APEX via the form. Then we declare l_keys as an array used to store strings. (If you're curious, the "l" prefix is used to say "local". Seems superfluous, but it's Oracle's convention, so...)
DECLARE
    l_request_body_clob CLOB;
    l_keys APEX_T_VARCHAR2 := APEX_T_VARCHAR2();
BEGIN

END;


body_text refers to the data sent in the form, via the API endpoint. This value is assigned to the variable l_request_body_clob.
DECLARE
    l_request_body_clob CLOB;
    l_keys APEX_T_VARCHAR2 := APEX_T_VARCHAR2();
BEGIN
    l_request_body_clob := :body_text;
END;


Then we use the parse() method of the APEX_JSON object, passing in l_request_body_clob as the p_source parameter's value. This, in effect, parses the form body data.
DECLARE
    l_request_body_clob CLOB;
    l_keys APEX_T_VARCHAR2 := APEX_T_VARCHAR2();
BEGIN
    l_request_body_clob := :body_text;

    APEX_JSON.parse(p_source => l_request_body_clob);
END;


Now for l_keys. We want to get the keys from the answers object. So we use the get_members() method of the APEX_JSON object (which already parsed the form data) and specify that the name of the object is "answers" by setting that as the parameter value of p_path. This in effect produces an array of all the keys in the form data, and binds that value to the array l_keys.
DECLARE
    l_request_body_clob CLOB;
    l_keys APEX_T_VARCHAR2 := APEX_T_VARCHAR2();
BEGIN
    l_request_body_clob := :body_text;

    APEX_JSON.parse(p_source => l_request_body_clob);

    l_keys := APEX_JSON.get_members(p_path => 'answers');
END;


To be safe, we have an IF block to check that l_keys is a valid non-empty array. Then we iterate through l_keys.
DECLARE
    l_request_body_clob CLOB;
    l_keys APEX_T_VARCHAR2 := APEX_T_VARCHAR2();
BEGIN
    l_request_body_clob := :body_text;

    APEX_JSON.parse(p_source => l_request_body_clob);

    l_keys := APEX_JSON.get_members(p_path => 'answers');

    IF l_keys IS NOT NULL AND l_keys.COUNT > 0 THEN
        FOR i IN 1..l_keys.COUNT LOOP

            DECLARE

            BEGIN


            END;

        END LOOP;
    END IF;
END;


We'll declare the serial number and answer here, in the variables l_serial_no and l_answer_value respectively. We know that those are just numbers and they won't go above 10, so "VARCHAR2(2)" is safe enough.
DECLARE
    l_request_body_clob CLOB;
    l_keys APEX_T_VARCHAR2 := APEX_T_VARCHAR2();
BEGIN
    l_request_body_clob := :body_text;

    APEX_JSON.parse(p_source => l_request_body_clob);

    l_keys := APEX_JSON.get_members(p_path => 'answers');

    IF l_keys IS NOT NULL AND l_keys.COUNT > 0 THEN
        FOR i IN 1..l_keys.COUNT LOOP
            DECLARE
                l_serial_no VARCHAR2(2);
                l_answer_value VARCHAR2(2);
            BEGIN

            END;
        END LOOP;
    END IF;
END;


Then we assign the value of the current element of l_keys, to l_serial_no. And we use the get_varchar2() method of APEX_JSON, again using "answers." and the current value of l_serial_number ("||" is actually concatenation in PL/SQL, so we're trying to read the value of answers.1, answers.2, and so on.) as the value of p_path, and assign the value to l_answer_value. Phew! That was a mouthful. But you get the idea... I hope.
DECLARE
    l_request_body_clob CLOB;
    l_keys APEX_T_VARCHAR2 := APEX_T_VARCHAR2();
BEGIN
    l_request_body_clob := :body_text;

    APEX_JSON.parse(p_source => l_request_body_clob);

    l_keys := APEX_JSON.get_members(p_path => 'answers');

    IF l_keys IS NOT NULL AND l_keys.COUNT > 0 THEN
        FOR i IN 1..l_keys.COUNT LOOP
            DECLARE
                l_serial_no VARCHAR2(2);
                l_answer_value VARCHAR2(2);
            BEGIN
                l_serial_no := l_keys(i);
                l_answer_value := APEX_JSON.get_varchar2(p_path => 'answers.' || l_serial_no);
            END;
        END LOOP;
    END IF;
END;


And we write an INSERT statement that adds a row with the values of l_serial_no and l_answer_value. Because QUESTION_SERIAL_NO is an integer, we need to use the TO_NUMBER() function on l_serial_no. POLL_ID will be the variable id in the POST handler.
DECLARE
    l_request_body_clob CLOB;
    l_keys APEX_T_VARCHAR2 := APEX_T_VARCHAR2();
BEGIN
    l_request_body_clob := :body_text;

    APEX_JSON.parse(p_source => l_request_body_clob);

    l_keys := APEX_JSON.get_members(p_path => 'answers');

    IF l_keys IS NOT NULL AND l_keys.COUNT > 0 THEN
        FOR i IN 1..l_keys.COUNT LOOP
            DECLARE
                l_serial_no VARCHAR2(2);
                l_answer_value VARCHAR2(2);
            BEGIN
                l_serial_no := l_keys(i);
                l_answer_value := APEX_JSON.get_varchar2(p_path => 'answers.' || l_serial_no);

                INSERT INTO POLL_RESULTS (POLL_ID, QUESTION_SERIAL_NO, RESULT)
                VALUES (:id, TO_NUMBER(l_serial_no), l_answer_value);
            END;
        END LOOP;
    END IF;
END;


The we use the open_object(), write() and close_object() of APEX_JSON to set status and message as a response.
DECLARE
    l_request_body_clob CLOB;
    l_keys APEX_T_VARCHAR2 := APEX_T_VARCHAR2();
BEGIN
    l_request_body_clob := :body_text;

    APEX_JSON.parse(p_source => l_request_body_clob);

    l_keys := APEX_JSON.get_members(p_path => 'answers');

    IF l_keys IS NOT NULL AND l_keys.COUNT > 0 THEN
        FOR i IN 1..l_keys.COUNT LOOP
            DECLARE
                l_serial_no VARCHAR2(255);
                l_answer_value VARCHAR2(4000);
            BEGIN
                l_serial_no := l_keys(i);
                l_answer_value := APEX_JSON.get_varchar2(p_path => 'answers.' || l_serial_no);

                INSERT INTO POLL_RESULTS (POLL_ID, QUESTION_SERIAL_NO, RESULT)
                VALUES (:id, TO_NUMBER(l_serial_no), l_answer_value);
            END;
        END LOOP;
    END IF;

    APEX_JSON.open_object;
    APEX_JSON.write('status', 'success');
    APEX_JSON.write('message', 'Answers processed successfully');
    APEX_JSON.close_object;

END;


Then we have a provision for if anything goes wrong.
DECLARE
    l_request_body_clob CLOB;
    l_keys APEX_T_VARCHAR2 := APEX_T_VARCHAR2();
BEGIN
    l_request_body_clob := :body_text;

    APEX_JSON.parse(p_source => l_request_body_clob);

    l_keys := APEX_JSON.get_members(p_path => 'answers');

    IF l_keys IS NOT NULL AND l_keys.COUNT > 0 THEN
        FOR i IN 1..l_keys.COUNT LOOP
            DECLARE
                l_serial_no VARCHAR2(255);
                l_answer_value VARCHAR2(4000);
            BEGIN
                l_serial_no := l_keys(i);
                l_answer_value := APEX_JSON.get_varchar2(p_path => 'answers.' || l_serial_no);

                INSERT INTO POLL_RESULTS (POLL_ID, QUESTION_SERIAL_NO, RESULT)
                VALUES (:id, TO_NUMBER(l_serial_no), l_answer_value);
            END;
        END LOOP;
    END IF;

    APEX_JSON.open_object;
    APEX_JSON.write('status', 'success');
    APEX_JSON.write('message', 'Answers processed successfully');
    APEX_JSON.close_object;

    EXCEPTION
        WHEN OTHERS THEN

            APEX_JSON.open_object;
            APEX_JSON.write("status", "error");
            APEX_JSON.write("message", "PL/SQL Error: " || SQLERRM);

            APEX_JSON.close_object;
END;


Time to test this! Fill in the poll and click SEND.


You should see this!


And the results in Oracle APEX's database, in the POLL_RESULTS table!


One more thing...

An anti-CSFR token is very easy to apply in Ruby On Rails. Just go to this file. When you rerun your form, you should see a hidden field if you view the source. Everything else is taken care of, including the validation.

app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
    allow_browser versions: :modern
    protect_from_forgery with: :exception
end


Next

Viewing the results, and testing.

Friday, 19 December 2025

Web Tutorial: Ruby On Rails Xmas Poll (Part 2/4)

For this part of the web tutorial, you'll need to have Rails installed. Here, you'll start a new project in the CLI using the rails new command followed by the name of your project. We'll add "--skip-active-record" because we won't be using the built-in database functionnality.
rails new xmas-poll-2025 --skip-active-record


Go to the file named gemfile and add these two lines. We are going to install httparty and dotenv-rails. httparty allows us to call endpoints, which is going to be absolutely necessary. dotenv-rails helps us get variables from the .env file. This is not so necessary, but it's a good practice and we're gonna do it.

Gemfile
gem "httparty"
gem "dotenv-rails", groups: [:development, :test]


Then navigate to the xmas-poll-2025 directory and run this command in the CLI.
bundle install


Still in the CLI, run this command to create the controller poll.
rails generate controller poll


This will create the file, poll_controller.rb, in the controllers directory of the app directory. Some other files will be created, but we can examine them later. Let's first make sure that we have an index action defined, and a submit action.

app/controllers/poll_controller.rb
class PollController < ApplicationController
    def index

    end

    def submit

    end
end


Go to routes.rb in the config directory. Make sure your main route is defined. Add a submit route for the form we're about to create. Note that we have the alias "poll_page" for the main route, and the alias "submit_poll_form" for the form submission route. These will be useful as references. We'll also define the index action of the poll controller as the root.

config/routes.rb
Rails.application.routes.draw do
    get "poll/", to: "poll#index", as: "poll_page"

    root "poll#index"
    post "poll/submit", to: "poll#submit", as: "submit_poll_form"
end


In the views directory, navigate to layouts. You should see application.html.erb. Change the title and add placeholders for alerts and notices.

app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
    <head>
        <title><%= content_for(:title) || "Xmas Poll 2025" %></title>
        <meta name="viewport" content="width=device-width,initial-scale=1">
        <meta name="apple-mobile-web-app-capable" content="yes">
        <meta name="mobile-web-app-capable" content="yes">
        <%= csrf_meta_tags %>
        <%= csp_meta_tag %>

        <%= yield :head %>

        <%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %>
        <%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %>

        <link rel="icon" href="/icon.png" type="image/png">
        <link rel="icon" href="/icon.svg" type="image/svg+xml">
        <link rel="apple-touch-icon" href="/icon.png">

        <%# Includes all stylesheet files in app/assets/stylesheets %>
        <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
        <%= javascript_importmap_tags %>
    </head>

    <body>
        <% if flash[:notice] %>
            <div class="notice"><%= flash[:notice] %></div>
        <% end %>
        <% if flash[:alert] %>
            <div class="alert"><%= flash[:alert] %></div>
        <% end %>
        <br />
        <%= yield %>
    </body>
</html>


We'll create a view for poll. In the views directory, create the poll directory and in it, create index.html.erb.

In here, we'll want to use the form helper. We can manually write the HTML form, but this really just defeats the purpose of using Ruby On Rails. The form will use the route submit_poll_form, which we defined earlier. Add a button too.

app/views/poll/index.html.erb
<div>
  <%= form_with url: submit_poll_form_path, local: true do |form| %>
    <button>Send</button>
  <% end %>
</div>


Run this command in the CLI.
rails server


When you go to http://localhost:3000, you should see this!


Let's just spruce this up a bit. Go to this CSS file. Here, set the font. Background color we'll keep to a Christmassy deep red. Create pollform. I want to give it a nice cheerful bright green background, with round corners. The button will be a big red one. Obviously, all this is just visual and makes no difference to functionality.

app/assets/stylesheets/application.css
body
{
  font-family: Verdana;
  font-size: 14px;
  background-color: rgb(100, 0, 0);
}

.pollform
{
  width: 600px;
  padding: 10px;
  border-radius: 10px;
  border: 3px solid rgb(200, 0, 0);
  background-color: rgb(0, 200, 0);
  margin: 5% auto 0 auto;
}

button
{
  width: 10em;
  height: 2em;
  margin-top: 10%;
  float: right;
  font-size: 1.5em;
  border-radius: 20px;
  border: 3px solid rgb(255, 2525, 255);
  background-color: rgb(255, 0, 0);
  color: rgb(255, 255, 255);
}

button:hover
{
  background-color: rgb(155, 0, 0);
}


In here, set the div class to pollform.

app/views/poll/index.html.erb
<div class="pollform">
  <%= form_with url: submit_poll_form_path, local: true do |form| %>
    <button>Send</button>
  <% end %>
</div>


There you go. Nothing in the form yet, but you can see that the form details will appear in the bright green area.


Time to work on the controller. Remember installing httparty? We'll use it here with an import.

app/controllers/poll_controller.rb
require "httparty"

class PollController < ApplicationController
    def index

    end

    def submit

    end
end


Before we continue, we need to add this line to the .env file. This is the URL of the API endpoint we defined in the previous part of the web tutorial.

.env
ORDS_API_URL=https://oracleapex.com/ords/teochewthunder/polls/poll/1


Then use the variable ORDS_API_URL from .env, in this way.

app/controllers/poll_controller.rb
require "httparty"

class PollController < ApplicationController
    ORDS_API_URL = ENV["ORDS_API_URL"]

    def index

    end

    def submit

    end
end


For index, we want to fetch the questions. Use the get() method of HTTParty, passing in ORDS_API_URL as an argument. This makes a call to that specific URL using a GET request. The result should be assigned to the variable response.

app/controllers/poll_controller.rb
class PollController < ApplicationController
    ORDS_API_URL = ENV["ORDS_API_URL"]

    def index
        response = HTTParty.get(
            ORDS_API_URL
        )
    end

    def submit

    end
end


The fetched results should look like this.
{
  "items": [
    {
      "name": "Xmas Poll 2025",
      "serial_no": 1,
      "title": "JOY TO THE WORLD"
    },
    {
      "name": "Xmas Poll 2025",
      "serial_no": 2,
      "title": "IT CAME UPON THE MIDNIGHT CLEAR"
    },
    {
      "name": "Xmas Poll 2025",
      "serial_no": 3,
      "title": "I SAW MOMMY KISSING SANTA CLAUS"
    },
    {
      "name": "Xmas Poll 2025",
      "serial_no": 4,
      "title": "HARK THE HERALD ANGELS SING"
    },
    {
      "name": "Xmas Poll 2025",
      "serial_no": 5,
      "title": "DECK THE HALLS"
    },
    {
      "name": "Xmas Poll 2025",
      "serial_no": 6,
      "title": "THE FIRST NOEL"
    },
    {
      "name": "Xmas Poll 2025",
      "serial_no": 7,
      "title": "RUDOLPH THE RED-NOSED REINDEER"
    },
    {
      "name": "Xmas Poll 2025",
      "serial_no": 8,
      "title": "SILENT NIGHT"
    },
    {
      "name": "Xmas Poll 2025",
      "serial_no": 9,
      "title": "JINGLE BELLS"
    },
    {
      "name": "Xmas Poll 2025",
      "serial_no": 10,
      "title": "JINGLE BELL ROCK"
    }
  ],
  "hasMore": false,
  "limit": 25,
  "offset": 0,
  "count": 10,
  "links": [
    {
      "rel": "self",
      "href": "https://oracleapex.com/ords/teochewthunder/polls/poll/1"
    },
    {
      "rel": "edit",
      "href": "https://oracleapex.com/ords/teochewthunder/polls/poll/1"
    },
    {
      "rel": "describedby",
      "href": "https://oracleapex.com/ords/teochewthunder/metadata-catalog/polls/poll/item"
    },
    {
      "rel": "first",
      "href": "https://oracleapex.com/ords/teochewthunder/polls/poll/1"
    }
  ]
}


With that in mind, we'll want to send that data to the view if called successfully. If unsuccessful, we'll want to flash a notice.

app/controllers/poll_controller.rb
def index
      response = HTTParty.get(
          ORDS_API_URL
      )

    if response.code == 200

    else
        flash.now[:alert] = "Error fetching data:"

        @api_data = {}
    end
end


We can actually test this. Deliberately sabotage the endpoint like this.

app/controllers/poll_controller.rb
response = HTTParty.get(
    ORDS_API_URL + "/test"
)


Here you go, a negative result!


Let's just style the notices. We want them to be a slim bar at the top, thus we set the position property to absolute. Let's have a nice green color scheme for a general success message and a red color scheme for errors.

app/assets/stylesheets/application.css
button:hover
{
  background-color: rgb(155, 0, 0);
}

.notice, .alert
{
  width: 100%;
  position: absolute;
  font-size: 0.85em;
  font-weight: bold;
  padding: 0.5em;
  text-align: center;
  left: 0;
  top: 0;
}

.notice
{
  background-color: rgba(100, 255, 100, 0.2);
  color: rgb(0, 255, 0);
}

.alert
{
  background-color: rgba(255, 100, 100, 0.2);
  color: rgb(255, 0, 0);
}


There it is!


Now un-sabotage the API endpoint. We want it to be correct. Make sure that the parsed_response property of response is assigned to api_data. Note that the "@" denotes api_data as an instance variable that can be used in the corresponding view.

app/controllers/poll_controller.rb
def index
    response = HTTParty.get(
        ORDS_API_URL
    )

    if response.code == 200
        @api_data = response.parsed_response
    else
        flash.now[:alert] = "Error fetching data: #{response.body}"
        @api_data = {}
    end
end


Then we'll work on the view. Remember api_data and what the returned JSON looked like? Well, if we want the Poll Title, we just need to use the first element of items, and get the name property.

app/views/poll/index.html.erb
<h1><%= @api_data["items"][0]["name"] %></h1>
<div class="pollform">
  <%= form_with url: submit_poll_form_path, local: true do |form| %>

  <% end %>
</div>


In the CSS, add a styling for h1 tags.

app/assets/stylesheets/application.css
body
{
  font-family: Verdana;
  font-size: 14px;
  background-color: rgb(100, 0, 0);
}

h1
{
  text-align: center;
  color: rgba(255, 255, 255, 0.5);
}


.pollform
{
  width: 600px;
  padding: 10px;
  border-radius: 10px;
  border: 3px solid rgb(200, 0, 0);
  background-color: rgb(0, 200, 0);
  margin: 5% auto 0 auto;
}


And here is the header!


Now we'll want to display the rest of api_data. Let's have a table in there, with these headers.

app/views/poll/index.html.erb
<h1><%= @api_data["items"][0]["name"] %></h1>
<div class="pollform">
  <%= form_with url: submit_poll_form_path, local: true do |form| %>
    <table>
      <tr>
        <td width="300px"><b>Carol</b></td>
        <td width="600px"><b>Rating (1 = lowest, 5 = highest)</b></td>
      </tr>
    </table>

    <button>Send</button>
  <% end %>
</div>


So far so good...


Then we use the each keyword on items, to iterate through it. Each instance of items will be known as question within this loop. And for each instance, we will have a HTML table row and two columns.

app/views/poll/index.html.erb
<h1><%= @api_data["items"][0]["name"] %></h1>
<div class="pollform">
  <%= form_with url: submit_poll_form_path, local: true do |form| %>
    <table>
      <tr>
        <td width="300px"><b>Carol</b></td>
        <td width="600px"><b>Rating (1 = lowest, 5 = highest)</b></td>
      </tr>
      <% @api_data["items"].each do |question| %>
        <tr>
          <td></td>
          <td></td>
        </tr>
      <% end %>

    </table>
    <button>Send</button>
  <% end %>
</div>


Here, we'll reflect the question property value for title.

app/views/poll/index.html.erb
<h1><%= @api_data["items"][0]["name"] %></h1>
<div class="pollform">
  <%= form_with url: submit_poll_form_path, local: true do |form| %>
    <table>
      <tr>
        <td width="300px"><b>Carol</b></td>
        <td width="600px"><b>Rating (1 = lowest, 5 = highest)</b></td>
      </tr>
      <% @api_data["items"].each do |question| %>
        <tr>
          <td><%= form.label nil, "#{question['title']}" %></td>
          <td></td>
        </tr>
      <% end %>
    </table>
    <button>Send</button>
  <% end %>
</div>


And here all the questions from the poll, the names of the carols, are displayed!


Next we want to display a series of radio buttons. The values will be from 1 to 5. To that end, we should define rating_options as a collection containing values 1, 2, 3 4 and 5. Then we use the to_a() method to convert it to an array.

app/views/poll/index.html.erb
<h1><%= @api_data["items"][0]["name"] %></h1>
<div class="pollform">
  <%= form_with url: submit_poll_form_path, local: true do |form| %>
    <% rating_options = (1..5).to_a %>
    <table>
      <tr>
        <td width="300px"><b>Carol</b></td>
        <td width="600px"><b>Rating (1 = lowest, 5 = highest)</b></td>
      </tr>
      
      <% @api_data["items"].each do |question| %>
        <tr>
          <td><%= form.label nil, "#{question['title']}" %></td>
          <td></td>
        </tr>
      <% end %>
    </table>
    <button>Send</button>
  <% end %>
</div>


In here, we want to iterate through rating_options using each. Each element will be referred to as option.
app/views/poll/index.html.erb
<tr>
  <td><%= form.label nil, "#{question['title']}" %></td>
  <td>
    <% rating_options.each do |option| %>

    <% end %>

  </td>
</tr>


In here, we use the form helper object radio_button. We pass in option as its value. Since it's a form element, it would be proper to give it name and id properties as well, which we will base on the value of the property serial_no, which is unique in each poll. The default value checked is always 3. You may notice ":none", passed in as the first argument. You can't omit this; I've tried and the server complains. This is actually meant to be the name of the argument, but I've overwritten it later as you can see, and now ":none" is just a placeholder value.

app/views/poll/index.html.erb
<tr>
  <td><%= form.label nil, "#{question['title']}" %></td>
  <td>
    <% rating_options.each do |option| %>
      <%= form.radio_button :none,
        option,
        name: "answers[#{question['serial_no']}]",
        id: "answer_#{question['serial_no']}_#{option}",
        checked: option == 3
      %>

    <% end %>
  </td>
</tr>


And now we have a label which displays option as the text, and preferences the element with the id specified, e.g, answer_2_1.

app/views/poll/index.html.erb
<tr>
  <td><%= form.label nil, "#{question['title']}" %></td>
  <td>
    <% rating_options.each do |option| %>
      <%= form.radio_button :none,
        option,
        name: "answers[#{question['serial_no']}]",
        id: "answer_#{question['serial_no']}_#{option}",
        checked: option == 3
      %>
      <%= form.label "answer_#{question['serial_no']}_#{option}", option %>
         
    <% end %>
  </td>
</tr>


So there, you see we have a bunch of radio buttons and labels!

Believe it or not, this is actually the easy part. Brace yourselves!

Next

Handling form submission.

Saturday, 12 April 2025

Buttons or Divs? What to use, and when

With the power of CSS, HTML is significantly more visually versatile than it was in its inception more than two decades ago. Especially with divs. You can make divs appear as anything - paragraphs, block quotes and images. In extreme examples, you could even render entire paintings using many, many divs. 

A huge variety of shapes,
especially rectangular.

The humble div tag, coupled with CSS, is no longer just a rectangle on the browser. Using properties such as transform, border-radius, width and height, among others, a web developer can achieve a myriad of looks.

And this manifests quite frequently, in buttons. Previously, I discussed whether button or input tags would be preferable, but today we make a separate comparison between divs and buttons.

Divs as buttons

Making divs look like buttons is simple enough. How about behavior? Well, for that, JavaScript accomplishes this fairly easily.

One is a button and the other is a div.
<button>This is a button</button>
<div style="cursor:pointer; background-color:rgb(230, 230, 230); border:1px solid rgb(100, 100, 100); border-radius: 3px; font-family: sans-serif; font-size: 12px; width: 8em; padding: 0.2em; text-align: center">
This is a div
</div>


But they can both be made to perform certain actions on a click.
<button onclick="alert('I am a button');">This is a button</button>
<div onclick="alert('I am a div');" style="cursor:pointer; background-color:rgb(230, 230, 230); border:1px solid rgb(100, 100, 100); border-radius: 3px; font-family: sans-serif; font-size: 12px; width: 8em; padding: 0.2em; text-align: center">
This is a div
</div>


Depending on the browser, you should see no appreciable difference between these.
This is a div


How about submitting a form? Well, a button usually does this.
<form id="frmTest">
    <button>Submit<button>
</form>


But if you want a div to do this, all you really need is a bit more code.
<form id="frmTest">
    <div onclick="document.getElementById('frmTest').submit()">Submit<div>
</form>


Definitely possible, but should we?

Visually, there's not a lot of difference. In fact, styling divs to look like buttons, could even potentially offset visual differences of button rendering between browsers. For example, we take the code written earlier.

This is how it looks on Chrome.


This is how it looks on Safari. See? There's no visual change in the div we styled, but the button looks remarkably different.


However, not everything is about the visual. Especially not to the visually-impaired. The button tag and a div tag reads differently in semantics. On a screen reader, the button tag immediately stands out as a control to be clicked, while a div is semantically no different from any other div.

That is the greatest, and most significant difference. Not being blind, it is understandably difficult to imagine perceiving anything other than in visual terms, since a large part of what people like us perceive, is in the visual medium.

Conclusion

The internet was not only made for people like myself. The internet was meant as an equalizer where it came to information access. Not only did it mean that the average person now had access to information that was not available readily in the past, people with visual disabilities were supposed to be able to access this information.

And that access could be compromised if code was written with the visual intent in mind rather than the semantic.


T___T

Monday, 27 January 2025

Web Tutorial: The NodeJS Wood Snake Fortune Teller (Part 2/2)

Time to start submitting the form!

Before anything, we use this module to parse the form body. We also want to use the form data to call an API endpoint, and for that we need the Fetch module. There are various versions of Fetch, and for the simplest one, we'll use a slightly backdated version.
npm install --save body-parser
npm install --save node-fetch@2


In the bdChange() function, add this line after displaying "Please wait...". It submits the form.

assets/js/functions.js
function bdChange() {
  var errorContainer = document.getElementById("errorContainer");
  var txtBd = document.getElementById("txtBd");  
  var formFortune = document.getElementById("formFortune");
  var fortuneContainer = document.getElementById("fortuneContainer");

  var d = new Date();
  var bd = new Date(txtBd.value);

  if (bd.getTime() > d.getTime()) {
    errorContainer.innerHTML = "Error! You can't possibly be born in the future.";
  } else {
    errorContainer.innerHTML = "";
    fortuneContainer.innerHTML = "Please wait..."
    formFortune.submit();
  }
}


Now, in app.js, we'll work on the route fortune. We first want to include the Fetch module.

app.js
app.post("/fortune", (req, res)=> {
  let fetch = require("node-fetch");
});


Declare headers an an object. We are preparing to send data via an OpenAI API endpoint. For that, the object needs to have the properties Authorization, OpenAI-Organization and Content-Type.

app.js
app.post("/fortune", (req, res)=> {
  let fetch = require("node-fetch");

  var headers = {
    "Authorization": ,
    "OpenAI-Oganization": ,
    "Content-Type":     
  }; 
 
});


Content-Type is JSON. The other two are strings based on your OpenAI account. As for what api is, it's a file we will create right now.

app.js
app.post("/fortune", (req, res)=> {
  let fetch = require("node-fetch");

  var headers = {
    "Authorization": "Bearer " + api.key,
    "OpenAI-Oganization": api.org,
    "Content-Type": "application/json"    
  };  
});


Having the details in this file helps protect your details. This information should be private.

api.js
module.exports = {
  org: "org-xxx",
  key: "sk-xxx"
}


Add this line. That will import the details of api.js so that any information is obtainable by referencing the object api.

app.js
var api = require("./api.js");
var express = require("express");


Continuing on with the fortune route, we declare messages as an empty array. Then we declare obj as n object and push obj into messages.

app.js
app.post("/fortune", (req, res)=> {
  let fetch = require("node-fetch");

  var headers = {
    "Authorization": "Bearer " + api.key,
    "OpenAI-Oganization": api.org,
    "Content-Type": "application/json"    
  };

  var messages = [];
  var obj = {

  }
  messages.push(obj);

});


obj should have the role property, value set to "user". The content property is the prompt string that will be sent to OpenAI. In it, we will use the birth date passed from the form.

app.js
app.post("/fortune", (req, res)=> {
  let fetch = require("node-fetch");

  var headers = {
    "Authorization": "Bearer " + api.key,
    "OpenAI-Oganization": api.org,
    "Content-Type": "application/json"    
  };

  var messages = [];
  var obj = {
    "role": "user",
    "content" : "It is currently 2025. My birth date is " + req.body.txtBd + ". Using exactly 5 paragraphs give me my Chinese Zodiac and element I also want a personality profile for myself. Lastly, provide a fortune for love, money and health."

  }
  messages.push(obj);
});


In order to use the body object, however, we need to summon the power of the module body-parser.

app.js
app.set("view engine", "handlebars");
app.set("port", process.env.PORT || 3000);

app.use(require("body-parser")());
app.use(express.static("assets"));


Back to the fortune route, we declare body as an object with model, messages and max_tokens as properties. messages will be the array messages that we just worked on, model will be whatever ChatGPT model you want to use, and max_tokens will be a matter of how much resources you want to dedicate to this call.

app.js
app.post("/fortune", (req, res)=> {
  let fetch = require("node-fetch");

  var headers = {
    "Authorization": "Bearer " + api.key,
    "OpenAI-Oganization": api.org,
    "Content-Type": "application/json"    
  };

  var messages = [];
  var obj = {
    "role": "user",
    "content" : "It is currently 2025. My birth date is " + req.body.txtBd + ". Using exactly 5 paragraphs give me my Chinese Zodiac and element I also want a personality profile for myself. Lastly, provide a fortune for love, money and health."
  };
  messages.push(obj);

  var body = {
    "model": "gpt-3.5-turbo",
    "messages" : messages,
    "max_tokens" : 2500
  }

});


We will then use fetch. Pass in as a first argument, the URL endpoint.

app.js
app.post("/fortune", (req, res)=> {
  let fetch = require("node-fetch");

  var headers = {
    "Authorization": "Bearer " + api.key,
    "OpenAI-Oganization": api.org,
    "Content-Type": "application/json"    
  };  

  var messages = [];
  var obj = {
    "role": "user",
    "content" : "It is currently 2025. My birth date is " + req.body.txtBd + ". Using exactly 5 paragraphs give me my Chinese Zodiac and element I also want a personality profile for myself. Lastly, provide a fortune for love, money and health."
  };
  messages.push(obj);

  var body = {
    "model": "gpt-3.5-turbo",
    "messages" : messages,
    "max_tokens" : 2500
  }

  fetch("https://api.openai.com/v1/chat/completions")
});


For the second argument, we need an object with the properties method, headers and body. method is "POST". headers is the object headers which we defined earlier, and body will be a JSON string representation of the body object.

app.js
app.post("/fortune", (req, res)=> {
  let fetch = require("node-fetch");

  var headers = {
    "Authorization": "Bearer " + api.key,
    "OpenAI-Oganization": api.org,
    "Content-Type": "application/json"    
  };  

  var body = {
    "model": "gpt-3.5-turbo",
    "messages" : messages,
    "max_tokens" : 2500
  }

  fetch("https://api.openai.com/v1/chat/completions", {
    method: "POST",
    headers: headers,
    body: JSON.stringify(body)
  }
)
});


Once the data has been obtained, we use the then() method to handle it. We'll use the text() method to send the text representation of the data stream response further down the line.

app.js
fetch("https://api.openai.com/v1/chat/completions", {
  method: "POST",
  headers: headers,
  body: JSON.stringify(body)
})
.then(response => response.text())


We then chain another then() method call to handle the data, and a catch() method to handle errors.

app.js
fetch("https://api.openai.com/v1/chat/completions", {
  method: "POST",
  headers: headers,
  body: JSON.stringify(body)
})
.then(response => response.text())
.then(data => {

})
.catch(err => {

});  


If there's an error, we render the form view but with err in the error property.

app.js
fetch("https://api.openai.com/v1/chat/completions", {
  method: "POST",
  headers: headers,
  body: JSON.stringify(body)
})
.then(response => response.text())
.then(data => {

})
.catch(err => {
  res.render("form", { error: err, fortune: "" });
});  


Handling data, we first declare json_data and assign to it the JSON representation of the string data.

app.js
fetch("https://api.openai.com/v1/chat/completions", {
  method: "POST",
  headers: headers,
  body: JSON.stringify(body)
})
.then(response => response.text())
.then(data => {
  var json_data = JSON.parse(data);
})
.catch(err => {
  res.render("form", { error: err, fortune: "" });
});  


We will derive the content by getting the first element of the choices array of json_data, then accessing the message and content properties.

app.js
fetch("https://api.openai.com/v1/chat/completions", {
  method: "POST",
  headers: headers,
  body: JSON.stringify(body)
})
.then(response => response.text())
.then(data => {
  var json_data = JSON.parse(data);
  var html_content = json_data.choices[0].message.content;

})
.catch(err => {
  res.render("form", { error: err, fortune: "" });
});  


We'll format this properly in HTML by using the replaceAll() method to replace all line breaks with HTML breaks.

app.js
fetch("https://api.openai.com/v1/chat/completions", {
  method: "POST",
  headers: headers,
  body: JSON.stringify(body)
})
.then(response => response.text())
.then(data => {
  var json_data = JSON.parse(data);
  var html_content = json_data.choices[0].message.content.replaceAll("\n\n", "<br /><br />");
})
.catch(err => {
  res.render("form", { error: err, fortune: "" });
});  


And finally, render the form view with html_content as the value of the fortune property.

app.js
fetch("https://api.openai.com/v1/chat/completions", {
  method: "POST",
  headers: headers,
  body: JSON.stringify(body)
})
.then(response => response.text())
.then(data => {
  var json_data = JSON.parse(data);
  var html_content = json_data.choices[0].message.content.replaceAll("\n\n", "<br /><br />");
  res.render("form", { error: "", fortune: html_content });
})
.catch(err => {
  res.render("form", { error: err, fortune: "" });
});  


Oops, the br tags do appear, but as text!

Add an extra pair of curly brackets for the fortune placeholder, to indicate that this should be formatted.

views/form.handlebars
<h1>Welcome to the Wood Snake Fortune Teller!</h1>

<form action="/fortune" id="formFortune" method="POST">
  <label for="txtBd">Birth Date</label>
  <br />
  <input type="date" name="txtBd" id="txtBd" value="2025-01-01" onChange="bdChange();" />
</form>

<div class="error" id="errorContainer">{{ error }}</div>
<br />
<div class="fortune" id="fortuneContainer"> {{{ fortune }}}</div>


Fixed!

This was a simple exercise in API calls using NodeJS. Hope you enjoyed, pretty sure I did!


Blessssssed fortunessssssss,
T___T

Saturday, 25 January 2025

Web Tutorial: The NodeJS Wood Snake Fortune Teller (Part 1/2)

What'ssssssss up?! (see what I did there?)

It's about to be the Year of the Snake. Chinese New Year is upon us in a week. To that end, I'd like to continue my experimentations with NodeJS. Today, we're going to explore two separate things - how to manage assets in a NodeJS app, and how to call and retrieve data via a REST endpoint using the Fetch module.

To that end, we will be building a fortune teller app. Way to lean into the Chinese mysticism, eh?

Let's begin with some module installation. We will need Express and Handlebars for starters, just to render the layouts.
npm install --save express
npm install --save express-handlebars


Let's continue with a bit of file structure setup. We should have a directory called assets. Within, let's have three subfolders - img, css and js. No prizes for guesing what files they hold! Then create the views directory, and within it, have the layouts folder. Create these files in their respective folders.

views/layouts/main.handlebars
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>2025 Wood Snake Fortune Teller</title>
  </head>
  <body>
    {{{ body }}}  
  </body>
</html>


views/404.handlebars
<h1>404</h1>

<p>Not found!</p>


views/500.handlebars
<h1>500</h1>

<p>There was an error.</p>
<p><b>{{ errorMessage }}</b></p>


views/form.handlebars
<h1>Welcome to the Wood Snake Fortune Teller!</h1>

<form>

</form>


Those are all the views we're going to have. It's good to get them out of the way. Now let's work on the main app. Create app.js. This beginning code sets up Express as the middleware and Handlebars as the view engine. main.handlebars is specified as the layout template file.

app.js
var express = require("express");

var app = express();

var handlebars = require("express-handlebars").create({defaultLayout:"main"});
app.engine("handlebars", handlebars.engine);

app.set("view engine", "handlebars");
app.set("port", process.env.PORT || 3000);


We will then fill in routes for the default, the form handling POST route process, the 404 and 500 error handling views. At the end of it, we "start" the app by using the listen() method to listen on Port 3000.

app.js
app.set("view engine", "handlebars");
app.set("port", process.env.PORT || 3000);

app.get("/", (req, res)=> {

});

app.post("/fortune", (req, res)=> {

});

app.use((req, res, next)=> {
  res.status(404);
  res.render("404");
});

app.use((err, req, res, next)=> {
  res.status(500);
  res.render("500", { errorMessage: err.code });
});

app.listen(app.get("port"), ()=> {

});


Let's add some placeholders to this view.

views/form.handlebars
<h1>Welcome to the Wood Snake Fortune Teller!</h1>

<form>

</form>

{{ error }}
<br />
{{ fortune }}


For the default route, add this line to render the form view which we just modified.

app.js
app.get("/", (req, res)=> {
  res.render("form");
});


Add this object as a second argument, with the properties error and fortune, to mirror the placeholders.
app.js
app.get("/", (req, res)=> {
  res.render("form", { error: "", fortune: "" });
});


Then set the value of fortune as a string.

app.js
app.get("/", (req, res)=> {
  res.render("form", { error: "", fortune: "To get your fortune in the year 2025, please provide your birth date." });
});


Simple so far, right?

Let's link some styling and CSS, and images in. We first take this image and save it in the img folder of the assets directory.

assets/img/snake.jpg

Then we save styles.css in the css folder of the assets directory, and functions.js in the js folder of the assets directory. Keep the both balnk, for now. Next, we need to add this line to app.js. The static() method of express, with "assets" passed in as an argument, returns a reference to the directory specified. When that is passed into the use() method of app, that directory becomes the default location of every static file specified thereafter.

app.js
app.set("view engine", "handlebars");
app.set("port", process.env.PORT || 3000);

app.use(express.static("assets"));

app.get("/", (req, res)=> {
  res.render("form", { error: "", fortune: "To get your fortune in the year 2025, please provide your birth date." });
});


So if we add these lines, styles.css and functions.js would be links to the assets directory.

views/layouts/main.handlebars
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>2025 Wood Snake Fortune Teller</title>

    <link rel="stylesheet" type="text/css" href="css/styles.css">
    <script type="text/javascript" src="js/functions.js"></script>

  </head>
  <body>
    {{{ body }}}  
  </body>
</html>


Now, add these divs in the HTML. Your body placeholder should fit into the div styled using the body CSS class.

views/layouts/main.handlebars
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>2025 Wood Snake Fortune Teller</title>

    <link rel="stylesheet" type="text/css" href="css/styles.css">
    <script type="text/javascript" src="js/functions.js"></script>
  </head>
  <body>
    <div class="container">
      <div class="header">
        
      </div>

      <div class="body">

        {{{ body }}}  
      </div>    
    </div>

  </body>
</html>


Time to fill in the CSS file! I want this to be mobile-sized, so I've specified a modest width of 300 pixels. The margin property puts it in the middle of the screen if you view it on a larger screen. Font and alignment have been set, though those are more visual choices. I've given the div round corners, an set the overflow property to hidden so as to bring out the round corners, and even given the whole thing a shadow! The background color is a specific choice of pale grey - to match the background of snake.jpg as much as I can.

assets/css/styles.css
.container {
  width: 300px;
  font-size: 12px;
  font-family: Verdana;
  margin: 0 auto 20px auto;
  background-color: rgb(196, 195, 200);
  text-align: center;
  border-radius: 10px;
  overflow: hidden;
  box-shadow: 8px 8px 5px 2px rgba(0, 0, 0, 0.5);
}


...and we have this!

We'll style header and body next. header has a specified background image. Since assets is already the default root directory for all static content, we just need to go up one level from the css folder before going into img to find snake.jpg. body just has a smaller width and the padding property compensates for it.

assets/css/styles.css
.container {
  width: 300px;
  font-size: 12px;
  font-family: Verdana;
  margin: 0 auto 20px auto;
  background-color: rgb(196, 195, 200);
  text-align: center;
  border-radius: 10px;
  overflow: hidden;
  box-shadow: 8px 8px 5px 2px rgba(0, 0, 0, 0.5);
}

.header {
  width: 300px;
  height: 205px;
  background-repeat: no-repeat;
  background-size: cover;
  background-image: url("../img/snake.jpg");
}

.body {
  width: 280px;
  padding: 10px;
}


Here we go!


We'll work on the form next. Set it to call the fortune route when submitting and specify that the method is POST. Also, give it an id.

views/form.handlebars
<form action="/fortune" id="formFortune" method="POST">

</form>


Inside the form, we will need an input and label. The input will be of the type date and both name and id will be txtBd. The default value is the first day of 2025.

views/form.handlebars
<form action="/fortune" id="formFortune" method="POST">
  <label for="txtBd">Birth Date</label>
  <br />
  <input type="date" name="txtBd" id="txtBd" value="2025-01-01" />
</form>


Now in the CSS, let's style this. These are just to make things look good and don't really affect functionality.

assets/css/styles.css
.body {
  width: 280px;
  padding: 10px;
}

label {
  font-weight: bold;
}

input[type="date"] {
  font-size: 1.2em;
  width: 150px;
  height: 1.5em;
  padding: 0.5em;
}


Here's the input. The user is supposed to select a date from this.

Now let's do a bit of front-end scripting. In the input, set the onchange attribute to call bdChange().

views/form.handlebars
<form action="/fortune" id="formFortune" method="POST">
  <label for="txtBd">Birth Date</label>
  <br />
  <input type="date" name="txtBd" id="txtBd" value="2025-01-01" onChange="bdChange();" />
</form>


Add divs to enclose the error and fortune placeholders. The ids and classes are as follows.

views/form.handlebars
<form action="/fortune" id="formFortune" method="POST">
  <label for="txtBd">Birth Date</label>
  <br />
  <input type="date" name="txtBd" id="txtBd" value="2025-01-01" onChange="bdChange();" />
</form>

<div class="error" id="errorContainer">{{ error }}</div>
<br />
<div class="fortune" id="fortuneContainer">{{ fortune }}</div>


Now fill in the JavaScript file with the bdChange() function.

assets/js/functions.js
function bdChange() {

}


Declare these variables, referencing the divs, the form and the input.

assets/js/functions.js
function bdChange() {
  var errorContainer = document.getElementById("errorContainer");
  var txtBd = document.getElementById("txtBd");  
  var formFortune = document.getElementById("formFortune");
  var fortuneContainer = document.getElementById("fortuneContainer");

}


Then use JavaScript's Date class to declare d as today's date. And bd as the date from the input.

assets/js/functions.js
function bdChange() {
  var errorContainer = document.getElementById("errorContainer");
  var txtBd = document.getElementById("txtBd");  
  var formFortune = document.getElementById("formFortune");
  var fortuneContainer = document.getElementById("fortuneContainer");

  var d = new Date();
  var bd = new Date(txtBd.value);

}


Do a comparison using the getTime() method. If bd is later than today's date, display the error.

assets/js/functions.js
function bdChange() {
  var errorContainer = document.getElementById("errorContainer");
  var txtBd = document.getElementById("txtBd");  
  var formFortune = document.getElementById("formFortune");
  var fortuneContainer = document.getElementById("fortuneContainer");

  var d = new Date();
  var bd = new Date(txtBd.value);

  if (bd.getTime() > d.getTime()) {
    errorContainer.innerHTML = "Error! You can't possibly be born in the future.";
  }

}


If not, clear the errorContainer div and put this message into the fortuneContainer div.

assets/js/functions.js
function bdChange() {
  var errorContainer = document.getElementById("errorContainer");
  var txtBd = document.getElementById("txtBd");  
  var formFortune = document.getElementById("formFortune");
  var fortuneContainer = document.getElementById("fortuneContainer");

  var d = new Date();
  var bd = new Date(txtBd.value);

  if (bd.getTime() > d.getTime()) {
    errorContainer.innerHTML = "Error! You can't possibly be born in the future.";
  } else {
    errorContainer.innerHTML = "";
    fortuneContainer.innerHTML = "Please wait..."
  }

}


Just a bit more styling...

assets/css/styles.css
.body {
  width: 280px;
  padding: 10px;
}

.error {
  color: rgb(200, 0, 0);
  font-size: 0.8em;
  font-weight: bold;
}

.fortune {
  border-top: 3px double rgb(100, 100, 100);
  padding-top: 10px;
}


label {
  font-weight: bold;
}

input[type="date"] {
  font-size: 1.2em;
  width: 150px;
  height: 1.5em;
  padding: 0.5em;
}


Try this with an error.


And without.


Next

Submitting the form and getting your fortune!