DEV Community

loading...

Class Journal - JavaScript and Rails Project

branmar97 profile image Brandon Marrero πŸ‡ΊπŸ‡Έ ・6 min read

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.

Index page

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
Enter fullscreen mode Exit fullscreen mode
class EntrySerializer
  include FastJsonapi::ObjectSerializer
  attributes :id, :title, :text, :author
end
Enter fullscreen mode Exit fullscreen mode

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

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}`)
        })
Enter fullscreen mode Exit fullscreen mode
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"
            }
}
Enter fullscreen mode Exit fullscreen mode

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.

Journal Comments

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

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

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

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

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.

Discussion (0)

pic
Editor guide