Introduction
I created a digital journal where users can create and delete journal entries, as well as comment on them. Think of it as a time capsule or diary that all students can read. This project uses a Rails backend, and JavaScript and Bootstrap on the frontend. My main goal with this project was to use my knowledge of Rails and JavaScript to build a CRUD application. Entries can only be created, read and deleted in the current state, however, I do plan on implementing an update entries feature in the future.
Project Requirements
There were some basic guidelines I needed to follow when building my application. Rails was required for the backend of this project. HTML, CSS and JavaScript were to be used for the frontend. I was required to have one has-many relationship, use classes to encapsulate my logic, and cover at least 2 of CRUD. I also needed to handle interactions between the client and server asynchronously using at least 3 AJAX calls while using JSON for the communication format. I fulfilled these requirements by creating and accessing entries from my Rails backend using serializers and fetch requests, and adding the information to the DOM on the frontend.
Rails Backend
Using Rails as an API for my project was very easy to setup. I finished this component with only 18 commits. My Rails backend has two models: Entry and Comment. Entry has title, author and text attributes. Comment has text, author and entry_id attributes, and belongs to Entry. This is how I fulfilled my has-many/belongs-to relationship requirement.
class EntriesController < ApplicationController
before_action :set_entry, only: [:show, :destroy]
def index
@entries = Entry.all
render json: @entries, except: [:created_at, :updated_at]
end
def show
render json: @entry , except: [:created_at, :updated_at]
end
def create
entry = Entry.create(entry_params)
render json: entry, status: 200
end
def destroy
@entry.destroy
end
private
def set_entry
@entry = Entry.find(params[:id])
end
def entry_params
params.require(:entry).permit(:title, :text, :author)
end
end
class EntrySerializer
include FastJsonapi::ObjectSerializer
attributes :id, :title, :text, :author
end
My entries controller features index, show, create and destroy actions. I utilized the fast_jsonapi gem for building out my serializers and creating formatted JSON responses for communication on the frontend with JavaScript. The comments controller features only index and create. Thanks to Rails and fast_jsonapi, my JSON was organized and easy to work with.
JavaScript Frontend
The JavaScript component was the most challenging part of this application. It was my first time building what I consider a complete Rails and JavaScript application. After I completed the backend I honestly did not know where to begin with my frontend.
I did many google searches, and viewed other projects and repos for examples. I decided to start with the index page, since I would need containers and a basic setup to manipulate the DOM. I then built my API Adapter, a class that made fetch requests to my Rails backend.
createEntry(entryTitle, entryAuthor, entryText) {
const entry = {
title: entryTitle,
author: entryAuthor,
text: entryText
}
return fetch(this.root + "/entries", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
},
body: JSON.stringify(entry)
})
.then(res => (res.json()))
.catch(error => console.log("Error: " + error))
}
With my fetch requests working, I started to build the Entry and Entries classes. Entry is in charge of instantiating and rendering entries, while Entries handles creating entries from form data, getting entries, and posting entries to the backend.
const entriesContainer = document.getElementById("entries-container")
// Build Entry Div
const entryDiv = document.createElement("div")
entryDiv.className = "entry-container mt-3 mb-5"
entryDiv.id = `entry-${this.id}-container`
entriesContainer.appendChild(entryDiv)
// Entry Title
const title = document.createElement("h3")
title.className = "entry-title"
title.innerText = this.title
entryDiv.appendChild(title)
// Entry Author
const author = document.createElement("p")
author.className = "entry-author"
author.innerHTML = `<i>${this.author}</i>`
entryDiv.appendChild(author)
// Entry Text
const text = document.createElement("p")
text.className = "entry-text"
text.innerText = this.text
entryDiv.appendChild(text)
// Comment Container
const commentsDiv = document.createElement("div")
commentsDiv.className = "entry-comment-container mt-5 mb-5"
commentsDiv.id = `entry-${this.id}-comment-container`
commentsDiv.style.display = "none"
// Show/Hide Comments
const showCommentsBtn = document.createElement("button")
showCommentsBtn.id = `entry-show-button-${this.id}`
showCommentsBtn.className = "btn btn-secondary me-1"
showCommentsBtn.innerHTML = "Comments"
showCommentsBtn.addEventListener("click", showHideComments.bind(this))
entryDiv.appendChild(showCommentsBtn)
// Delete Button
const deleteBtn = document.createElement("button")
deleteBtn.setAttribute("id", `delete-button-${this.id}`)
deleteBtn.className = "btn btn-danger me-1"
deleteBtn.innerHTML = "Delete"
entryDiv.appendChild(deleteBtn)
entryDiv.appendChild(commentsDiv)
deleteBtn.addEventListener("click", () => {
entryDiv.remove()
this.adapter.deleteEntry(`${this.id}`)
})
function showHideComments() {
const commentsDiv = document.getElementById(`entry-${this.id}-comment-container`)
if (commentsDiv.style.display === "none") {
commentsDiv.style.display = "block"
} else {
commentsDiv.style.display = "none"
}
}
I did not like how much screen real estate that comments was taking up, so I built a function that shows or hides comments on a button listener. This seemed to be a lot more user friendly and much easier to read.
Creating New Entries
The entries class was setup with form bindings and an event listener on the submit button, which triggers my create new entry method. It uses the form values to make a post request to the backend and instantiate new entry objects, then utilizes the responses to create entry objects on the frontend for rendering.
newEntryBindings() {
this.newEntryForm = document.getElementById("new-entry-form")
this.newEntryTitle = document.getElementById("new-entry-title")
this.newEntryAuthor = document.getElementById("new-entry-author")
this.newEntryText = document.getElementById("new-entry-text")
this.newEntryForm.addEventListener('submit', this.createNewEntry.bind(this));
}
createNewEntry(event) {
event.preventDefault()
const entryTitle = this.newEntryTitle.value
const entryAuthor = this.newEntryAuthor.value
const entryText = this.newEntryText.value
this.adapter.createEntry(entryTitle, entryAuthor, entryText)
.then(entry => {
const newEntry = new Entry(entry)
this.entries.push(newEntry)
this.newEntryTitle.value = " "
this.newEntryAuthor.value = " "
this.newEntryText.value = " "
newEntry.renderEntry()
})
}
Building Comments
The Comment and Comments classes were setup similarly to my Entry classes. Comment instantiates and renders comments to the DOM and Comments fetches and renders comments from the backend. Building this section was a lot of fun and a great learning experience. I learned how to display comment count by getting the number of children from the unordered list elements. It can also make the word "comment" singular or plural based on the count.
const commentCount = document.createElement("h5")
commentCount.id = `entry-${this.id}-comment-count`
commentCount.className = "mt-5 mb-3"
if (commentsUl.childElementCount === 1) {
commentCount.innerText = `${commentsUl.childElementCount} Comment`
} else {
commentCount.innerText = `${commentsUl.childElementCount} Comments`
}
commentsDiv.prepend(commentCount)
Async Issues
Later on into development, I stumbled upon a huge bug that I did not notice at first. Sometimes my comments were rendering, and other times they would fail to load. My entries were coming up as null.
I eventually found out that this was a timing issue. Initially, my application was running asynchronously in parallel. Like this:
new Entries()
new Comments()
The problem with this setup was that both classes were making fetch requests at the same time, which is not really ideal. There were also too many functions being called in my entries constructor.
My fetch requests for Entries were much larger, and the comments were coming back before the entries had finished loading. This was a major issue because the entries are parents of the comments, and without them the comments cannot render.
The solution was to add an event listener with DOMContentLoaded and a callback function that would not instantiate comments until entries are finished. I used "them" and an arrow function to make this work.
document.addEventListener("DOMContentLoaded", function() {
new Entries().fetchAndLoadEntries().then(() => {
new Comments()
})
})
Polishing
After the async fix I had a complete, functional project. I began to focus on polishing and making the frontend look prettier. Bootstrap made this step very easy. I styled entire headers, forms and lists in minutes.
Future Improvements
I plan on making a few changes to what can be done with entries. Currently, entries can only be read, created and deleted. I hope to have full CRUD capability for entries in the future.
Comment count can also be refactored. Instead of getting comment count by child element count, I can store entry comments in an array and get the array count to make my code more dynamic.
Conclusion
Building this project was a huge challenge and a learning experience. Not only did I become much more confident writing JavaScript, I utilized what I learned in my previous module with Rails, but in new ways. I can now build complete applications using JavaScript, Rails and Bootstrap with CRUD features. Two months ago I would not even know where to begin. I hope to take what I have learned and create even richer projects in the future.
Top comments (0)