Since my recent Ruby on Rails project on Plan My MD Visit, I have immersed myself in JavaScript Fundamentals from recognizing JavaScript events, DOM manipulation, ES6 Syntax Sugar and introduction of Object-Orientation. I plan to supplement my JavaScript learning materials after this project submission.
I went through a few iterations in my head on Single-Page Application (SPA) prior to settling on an idea. Overthinking as usual, but in my defense it is one HTML file, and lends itself too much freedom. 😅 Moving on, my husband loves trivia, and nothing is better than to surprise him by creating my own version of a trivia app, Know It All. The challenge becomes finding a completely free JSON API for use. This capstone project focuses on creating Ruby on Railsback-end and JavaScript/HTML/CSSfront-end.
With --api, Rails removes a lot of default features and middleware, and our controllers by default inherit from ActionController::API. This differs slightly from traditional Ruby on Rails application. In my previous RoR project, I had my controllers inheriting from ActionController::Base with responsibilities in creating routes and rendering many _.html.erb files.
rails new know_it_all_backend --database=postgresql --api
The above command will generate a Rails API using PostgreSQL database. The intent is to deploy my backend application eventually on Heroku, which does not support SQLite database. One more important thing to add is to bundle install gem rack-cors. This is useful for handling Cross-Origin Resource Sharing (CORS) configuration, allowing my front-end application to perform asynchronous requests.
I approached this project in a vertical manner, building out one model and/or feature at a time. This strategy streamlines any effort when dealing with complex relationships from back-end to front-end, and vice versa.
2. Open Trivia DB API
After traversing through the API universe, I got excited when finding an Open Trivia Database without the need for an API Key. Awesome sauce. 🙅🏻♀️
The challenge is less on acquiring the JSON API, but setting up the Api adapter class on Rails back-end. I utilized the .shuffle Ruby method to randomize the provided multiple choice. In the JavaScript front-end, I should be able to set up if/else conditionals when comparing the user's selected answer to the correct_answer. I managed to JSON.parse in irb, and confirmed responses back from the open/free API.
>data["results"][0]=>{"category"=>"Animals","type"=>"multiple","difficulty"=>"hard","question"=>"What was the name of the Ethiopian Wolf before they knew it was related to wolves?","correct_answer"=>"Simien Jackel","incorrect_answers"=>["Ethiopian Coyote","Amharic Fox","Canis Simiensis"]}>[data["results"][0]["correct_answer"],data["results"][0]["incorrect_answers"][0],data["results"][0]["incorrect_answers"][1],data["results"][0]["incorrect_answers"][2]].shuffle=>["Amharic Fox","Canis Simiensis","Simien Jackel","Ethiopian Coyote"]>multiple_choice=_=>["Amharic Fox","Canis Simiensis","Simien Jackel","Ethiopian Coyote"]>multiple_choice[0]=>"Amharic Fox">multiple_choice[1]=>"Canis Simiensis">multiple_choice[2]=>"Simien Jackel">multiple_choice[3]=>"Ethiopian Coyote"
There will be a total of eight (8) Trivia categories: Animals, Celebrities, Computer Science, Geography, History, Mathematics, Music and Sports. Once the Api adapter class was fully set up, I initiated the creation of both Category and Question models in the seeds.rb.
3. Generating Active Record Models
$ rails g model User name avatar animals_score:integer celebrities_score:integer computer_science_score:integer geography_score:integer history_score:integer mathematics_score:integer music_score:integer sports_score:integer
invoke active_record
create db/migrate/20210224154513_create_users.rb
create app/models/user.rb
$ rails g model Category name
invoke active_record
create db/migrate/20210224045712_create_categories.rb
create app/models/category.rb
$ rails g model Question category_id:integer question:text choice1 choice2 choice3 choice4 answer
invoke active_record
create db/migrate/20210227220035_create_questions.rb
create app/models/question.rb
In the terminal, I can now run rails db:create && rails db:migrate. The rails db:create is necessary for PostgreSQL database. At first, I had an erroneous terminal return, and had to update my PostgreSQL 13. Once re-installed and 🐘 running, the command should create the database and run migration swiftly.
The next step would be to test my models and associations. My association between Category and Question would be as simple as category has_many questions, and a question belongs_to a category.
The dependent: :destroy would be helpful for .destroy_all method in seeds.rb file. This is useful when triggering the rails db:seed command.
As an Active Record veteran, it is still a good practice to validate every single instance of association relationships. Note — presented model attributes resulted from extensive trial and error. I approached this project with one feature working simultaneously on the back-end and front-end, adding one model attribute at a time.
001 > animals = Category.create(name: "Animals")
(0.2ms) BEGIN
Category Create (4.8ms) INSERT INTO "categories" ("name", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id" [["name", "Animals"], ["created_at", "2021-02-28 18:30:29.016555"], ["updated_at", "2021-02-28 18:30:29.016555"]]
(40.4ms) COMMIT
=> #<Category id: 1, name: "Animals", created_at: "2021-02-28 18:30:29", updated_at: "2021-02-28 18:30:29">
002 > animals_trivia = animals.questions.create(JSON.parse(File.read("animals.json")))
(0.2ms) BEGIN
Category Create (4.8ms) INSERT INTO "categories" ("name", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id" [["name", "Animals"], ["created_at", "2021-02-28 18:30:29.016555"], ["updated_at", "2021-02-28 18:30:29.016555"]]
(40.4ms) COMMIT
=> #<Category id: 1, name: "Animals", created_at: "2021-02-28 18:30:29", updated_at: "2021-02-28 18:30:29">
(0.3ms) BEGIN
Question Create (4.8ms) INSERT INTO "questions" ("question", "choice1", "choice2", "choice3", "choice4", "answer", "category_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING "id" [["question", "What was the name of the Ethiopian Wolf before they knew it was related to wolves?"], ["choice1", "Canis Simiensis"], ["choice2", "Simien Jackel"], ["choice3", "Ethiopian Coyote"], ["choice4", "Amharic Fox"], ["answer", "Simien Jackel"], ["category_id", 1], ["created_at", "2021-02-28 18:30:42.398662"], ["updated_at", "2021-02-28 18:30:42.398662"]]
(55.1ms) COMMIT
(0.2ms) BEGIN
...
003 > animals_trivia.all.count
=> 50
4. Routes, Controllers and Serializers
Routes
With the front-end application hosted on a specific domain, I would think it is prudent to namespace my back-end routes. It provides an indication that these back-end routes are associated with the API. For example, https://knowitall.com/api/v1/categories. The api/v1 suggests my Rails API version 1. I might return and continue my effort on future build status (version 2, etc). In the config/routes.rb, I provided the intended namespaced routes and confirmed with rails routes command.
rails g controller api/Users, rails g controller api/v1/Questions and rails g controller api/v1/Categories create UsersController, QuestionsController and CategoriesController. These namespaced routes and their respective controllers nomenclature help tremendously in setting up filenames hierarchy.
Note — make sure the PostgreSQL 🐘 is running while configuring routes and controllers.
classApi::UsersController<ApplicationControllerdefindexusers=User.allrenderjson: UserSerializer.new(users)enddefcreateuser=User.create(user_params)ifuser.saverenderjson: UserSerializer.new(user),status: :acceptedelserenderjson: {errors: user.errors.full_messages},status: :unprocessable_entityendenddefshowuser=User.find_by(id: params[:id])ifuserrenderjson: userelserenderjson: {message: 'User not found.'}endenddefupdateuser=User.find_by(id: params[:id])user.update(user_params)ifuser.saverenderjson: userelserenderjson: {message: 'User not saved.'}endendprivatedefuser_paramsparams.require(:user).permit(:name,:avatar,:animals_score,:celebrities_score,:computer_science_score,:geography_score,:history_score,:mathematics_score,:music_score,:sports_score)endend
I will only have the UsersController displayed here, and briefly convey the render json. My rails routes only strictly render JSON strings. This is useful when building JavaScript front-end on DOM manipulation and performing asynchronous requests. The user_params on name, avatar and all category scores will be included in the body of POST and PATCH requests when executing fetch. status: :accepted helps to inform the user of the success 202 HTML status when submitting user input forms on the front-end application. If it fails to save, status: :unprocessable_entity notifies the client error 422 HTML status.
Serializers
gem 'fast_jsonapi' is a JSON serializer for Rails APIs. It allows us to generate serializer classes. The goal of a serializer class is to keep controllers clear of excess logic, including arranging my JSON data to display certain object attributes. It does not hurt to practice serializer early on, even though the current state of my Minimum Viable Product (MVP) does not necessarily require one.
5. Communicating with the Server
In order to make sure the Rails server back-end API worked, I tested a few Asynchronous JavaScript and XML (AJAX) calls on my browser console. While I have been using a lot of fetch() for this project, I have yet to challenge myself with async / await function. I am glad my initial attempt of fetch() in browser console made successful requests. Moving on to front-end!
The MVP of Know It All app is to create a basic Trivia game on Single-Page Application (SPA). This project is built with Ruby on Rails back-end and JavaScript front-end.
Know It All :: Back-End
Domain Modeling :: Trivia Games
Welcome to my simplistic version of Online Trivia Games.
I have to say this part is my most challenging part! I was struggling to gather all of my new knowledge of The Three Pillars of Web Programming: recognizing JavaScript events, Document Object Model (DOM) manipulation, and communicating with the server on a Single-Page Application (SPA). Separation of concerns, as a fundamental programming concept, is still applicable. HTML defines the structure of the website, JavaScript provides functionality and CSS defines the visual presentation.
1. DOM Manipulation with JavaScript Event Listeners
It took me a few days practicing on a set of hard-coded trivia questions and having my trivia cards update as the user progresses to the next question. Know It All includes a score tracker, questions quantity, progress bar, along with passing and/or failing User Interface (UI) alerts. Having Single-Page Application (SPA) required me to create an element with document.createElement('...') multiple times, and using either .append() or .appendChild() often. Also, trying to incorporate Bootstrap CSS early resulted in a slow and unproductive debugging process. A part of me loves spending gazillion hours on CSS elements. Note to self — do not waste your time on CSS! 😅
One particular challenge I found was to gather user input fields and update their back-end values with asynchronous JavaScript PATCH. Later I found that I got stuck on an erroneous fetch url, and corrected my string template literals to ${this.url}/${currentUser.id}. While I used a lot of standard and static methods in my OO JavaScript, I plan to explore both get and set methods.
2. Re-factoring Early
After spending sometime working on basic event handlings, my index.js file piled up easily with 200+ lines of code. While I have spent the past month on JavaScript Functional Programming, Object-Oriented (OO) JavaScript offers better data control, easy to replicate (with constructor method and new syntax), and grants us the ability to write code that convey these relationships. I decided to build classes and their execution contexts in separate files, api.js, category.js, user.js and question.js. Each class has its own lexical scope of variables and functions, leaving index.js with global variables and callback functions necessary to support index.html.
During this re-factoring exercise, I also removed all of my vars, and replaced them with either const or let. Variables declared with const and let are block-scoped.
3. End Page Sequence
Drum roll... 🥁 We are now coming close to an end. After each set of trivia questions, users should be able to see their final score, and whether or not they beat their previous score. If they do, the new (or higher) score will be saved in the Rails API user database. There will be two options for the user to either Play Again or return back to Home page.
4. Lessons Learned
After months of GitHubing, I am getting really comfortable with working on a separate branch, and merge to master. The git command git co -b <branch_name> becomes my go-to git command.
Understanding JavaScript's syntax and semantics after months dwelling on Ruby has been fun. For example, in JavaScript, functions are treated as first-class data, and understanding some of the concepts of hoisting and scope chain. JavaScript engine works in compilation phase and execution phase. Since I used a lot of JavaScript event click and submit for this project build, I would love to explore other browser events. This YouTube tutorial helped me tremendously to better understand the weird parts of JavaScript.
The MVP of Know It All app is to create a basic Trivia game on Single-Page Application (SPA). This project is built with Ruby on Rails back-end and JavaScript front-end.
Know It All :: Front-End
Domain Modeling :: Trivia Games
Welcome to my simplistic version of Online Trivia Games.
Know It All was completed in a 2-week timeframe from API data search, Ruby on Rails back-end, and JavaScript front-end User Interface. Future cycle of product development as follows:
Add Sub Category to model associations. User should be able to select Category and its Sub Category. For example, science category has many sub categories including physics, mathematics, biology and so on.
Outsource APIs for the aforementioned Sub Category trivia questions.
Gather user inputs on their favorite Category on future app improvement.
Utilize setInterval and/or time limit of 15-20 seconds on each Trivia question.
User authentication.
Create a toggle track for dark mode 😎
Post Scriptum:
This is my Module 3 capstone project with Flatiron School. I believe one of the catalyst to becoming a good programmer is to welcome constructive criticism. Feel free to drop a message. 🙂