How I built a SPA with Vanilla.js + Ruby on Rails backend

How I built a SPA with Vanilla.js + Ruby on Rails backend

What is a Single Page Application?

There are two main design patterns for web apps: multi-page application (MPA) and single-page application (SPA). A single-page application is an app that works inside a browser and does not require page reloading during use. Github, Gmail, and Hashnode. A multi-page application works in a more conventional way. Every change we make for example displaying the data or submitting data back to server requests renders a new page from the server in the browser. These applications are large, bigger than SPAs because they need to be. Multi-page apps are better for SEO because they are larger (and they also have more META tags). I built a SPA education strategy for app focused around where teachers can submit helpful strategies through a form and leave comments on what strategies were effective for students. No user authentication is required.

Communicating with a Server with a SPA

In order for the SPA running in the browser to communicate with a server, both need to speak the same language. The first order of business is deciding on the type of data that will be sent and received.

When you’re processing an asynchronous task, such as your server call to update comments for a strategy, you don’t always want the application to hang while you wait for the server to respond. You sometimes need it to continue in the background while your application handles other tasks. So instead of the update function returning a value when it’s done, callbacks are passed in to handle the results when it completes. You can do this because functions can be passed around. This allows any function to take other functions as arguments. I chose to use the ES2017 asynchronous async/await syntax to callback my promises.

Async functions are a combination of promises and are built on promises. With await'ing a promise, the function is has a breaking point until the promise is settled or else it will throw an error indefinitely.

See an example below, I'm setting my params under strongParams under my async function then awaiting the fetch request under my comments endpoint to post a new comment with an HTTP method POST


// create comment under associated strategy
const createComment = async (e) => {
    e.preventDefault();
   // create strong params for the comment
    const strongParams = {
        comment: {
        title: getComTitle().value,
        body: getComBody().value,
        }
    };

    const strategyId = document.getElementById('comments').getAttribute('strategyid')
    // post the comment to the comments endpoint with the associated strategy id
    const response = await fetch(baseUrl + `/strategies/${strategyId}/comments`, {

        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'Accept': 'application/json'
        },
        body: JSON.stringify(strongParams)
    }).catch(err => { alert(err) });
    const comment = await response.json();
    // push comment on the comments array
    comments.push(comment);
    renderComment(comment);
    getComTitle().value = '';
    getComBody().value = '';
    alert('Comment successfully added');
};

In order for me to be able to set my comment params, I have to make sure:

  1. My routes are nested correctly:
    resources :strategies do
    resources :comments
    end
    
  1. My strategy model in my Ruby on Rails API appending the following:
    has_many :comments, dependent: :destroy, foreign_key: :strategy_id
    

Manipulating the Dom x DRY principle

Getters give you a way to define a property of an object, but they do not calculate the property's value until it is accessed. In my application and a convention of the Don't Repeat Yourself (DRY) I set up node getters at the top for my index.js file to retrieve elements from the DOM. I rarely repeated myself to retrieve my elements id's and saved me a ton of time retrieving DOM elements.

const strategyForm = () => document.getElementById('strategy-form');
const commentForm = () => document.getElementById('comment-form');
const strategyFormSubmit = () => document.getElementById('submit');
const commentFormSubmit = () => document.getElementById('comment-submit');
const getStrategyName = () => document.getElementById('name');
const getStrategyReference = () => document.getElementById('reference');
const getStrategyTier = () => document.getElementById('tier');
const getStrategyCategory = () => document.getElementById('category');
const getStrategyDescription = () => document.getElementById('description');
const formHeader = () => document.getElementById('form-header');
const getComTitle = () => document.getElementById('comment-title');
const getComBody = () => document.getElementById('comment-body');
const strategyList = () => document.getElementById('strategies');
const commentList = () => document.getElementById('comments');
const scrollTop = () => document.documentElement.scrollTop = 0;

I hope this was helpful and if I missed any key points, please leave a note in the comments!

I always listen to some good vibes while I'm coding and here's some feel-good Ampiano I discovered recently from South Africa...