Creating Associated Objects in Single Page Applications

A few months ago, I had the idea to build a simple game that functions something like this.

An early, rough concept for a flash card/quick match game

The game mechanics changed in the early stages of building it in Ruby on Rails (Rails) and Javascript. One feature, I knew I wanted, was to be able to add a player’s name and their score to the leaderboard at the end of a game.

Since a User has_many Games I wanted to be able to create a new Game instance at the same time as a User instance. If a User with the given name already exists, create the Game and associate it with that User.

Oh and this had to happen without reloading the page.

Was it possible?

Yes!

At first, I was overthinking how it might work on the frontend. Then, a classmate pointed out, that the creation of the User and Game could be handled on the backend, since the two were associated.

For this to happen, I need two pieces of data

  • The player’s name from the form input
  • And their score

I could query the DOM for the input field with this.nameInput = document.querySelector("#name") and access it’s value with .value. The score was being rendered on the frontend so I could access it in a similar fashion.

const body = {
      score: this.score.innerText,
      name: this.nameInput.value,
    }

Formatted as an object, I could pass it into the createGame() fetch request in GamesAdapters.js.

  createGame(body) {
    return fetch(this.baseURL, {
      method: 'POST',
      headers: {
         'content-type': 'application/json'
      },
      body: JSON.stringify(body)
    })
    .then(res => res.json())
  }

Did I mention the page shouldn’t reload either?

Luckily, that could be solved by including e.preventDefault() within the createGame() function that is triggered by the form submission button.

createGame(e) {
    console.log("Saving the game...")

    e.preventDefault()
    
    const body = {
      score: this.score.innerText,
      name: this.nameInput.value,
    }

    this.adapter.createGame(body)

  }

Rendering the New Score

The POST fetch() request created a new record and returns the record object, which looks something like this.

#<Game id: 85, user_id: 1, score: 37, created_at: "2020-02-24 00:25:42", updated_at: "2020-02-24 00:25:42">

Oh no.

I see a user_id. Within Rails, it’s possible to get a Users name from their ID, but is there a way to access it on the front end without a page reload? There is! Since we were already asking a player to input their name into a field, on the frontend, we could use that, instead of querying the database.

There is, but it would require a page reload, which we want to avoid for this project. But, we can fake it on the UI by pulling the name from the input field and displaying that. Let’s add a .then statement to the end of createGame(e) to handle this.

createGame(e) {
    console.log("Saving the game...")

    e.preventDefault()

    const body = {
      score: this.score.innerText,
      name: this.nameInput.value,
    }

    this.adapter.createGame(body)

    .then(game => {
      let gameObject = `<li>${game.score} - ${this.nameInput.value}</li>`
      this.leaderboard.innerHTML += gameObject
      this.nameInput.value = ""
    })

  }

Our leaderboard, now looks something like this

A 3rd player and their score has been added to the leaderboard

If we were to refresh/reload the page, visually, we would not see anything different. This is an example of an optimistic user interface. We’re anticipating that the data is successfully written to the server and are responding right away by displaying the data. We’ll also clear out the input field when we’re done, to mimic what would happen if there were a page refresh.


Leave a comment

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.