-
Routes
-
Controllers
-
Serializers
-
Follow Up Question:
rails g migration AddCategoryToSyllabus category:belongs_to
will generate the following migration:
class AddCategoryToSyllabus < ActiveRecord::Migration def change add_reference :syllabuses, :category, null: false, foreign_key: true end end
- I WAS WRONG: Because we're using PostGres we need to delete
null: false
so this migration will be successful. Read why here. - SHOUT OUT TO WILLIAM: WE DO NOT need to delete
null: false
in this migration. But we do need to delete the syllabi seed data in therails console
before running the migration using:Syllabus.all.each{|s| s.destroy}
"I ran into the same issue when I had to a foreign key to one of my tables. It happens because we already seeded the database with records in the table which we are trying to add the foreign key to. As a result, when we run the migration, the database can't add the column because by default those records would then have `null` values for the foreign key. **I think it is important that a foreign key reference always be non-null to ensure the integrity of the database.** As a result, the "fix" I used (since I didn't want to just drop my database) was just to go into the Rails console and run `Word.all.each{|w| w.destroy}` to drop all of the words that would have had a `null` foreign key reference to the author model. After that, the migration ran and I was able to keep the null false check in PostgreSQL." — William Barela
Coming from the Rails Section, you may be used to a framework such as Ruby on Rails being very opinionated. That is, it has a lot of opinions about how your application should be structured. The same rules don't apply on the frontend, there is not one right way to structure your code. Specifically, we are not using any frontend framework and many of the design decisions will be left up to you.
In PART 3 of this project build, we'll walk through getting and creating data and provide some example code. The example code will demonstrate a reasonable/sensible way to structure this application. You should learn what you can from it and structure your code in a similar pattern.
The key word here is similar, rather than directly copying the patterns shown, try to apply the principles you have learned (oo, single responsibility principle, encapsulation) to make code that will be easy for you as your application grows.
Source: JavaScript Rails API Project Setup
Make sure you create a separate directory and a separate GitHub repository for the frontend.
Tip: you can open up a new tab in terminal command + t
if you'd like to have your rails server up and running in another tab.
Create a new folder for the frontend: mkdir notes_frontend && cd $_
(The cd $_
command will move you into the folder you've just created)
In the new folder you create you should touch index.html
and mkdir src
in which you will add your JavaScript files. At minimum you should have a file called index.js
inside of the src
folder.
In index.html
, you need to add some HTML. Text editors will often have a shortcut for creating a blank HTML document. In Atom you can begin typing "html" and then press tab to auto-generate the starter HTML.
The first step is getting the list of notes to show up on the page. Translating that to more technical language, we need to:
1 - Fire off an AJAX fetch request to the index route (i.e GET '/syllabuses')
2 - Use the response JSON to append elements to the DOM.
Let's be sure not to overcomplicate our approach, we can (and will) refactor later. At the same time, we don't want to be debugging the routes in our Rails application trying to figure why our server isn't responding when it turns out we forgot to include a script tag linking src/index.js
in index.html
. Speaking of which, don't forget to add <script src="src/index.js" charset="utf-8"></script>
to the head of your index.html
This may sound silly but step 1 should be:
/* src/index.js */
document.addEventListener('DOMContentLoaded', () => {
alert('LOADED');
});
Until you see the alert, don't move forward. What would be some reasons you might not see the alert? You can also console.log("in index.js")
in index.js
file.
Before we make our first fetch request, we need to talk about something called Cross Origin Resource Sharing (CORS):
- Basically, CORS is a security feature that prevents API calls from unknown origins. For example, if someone tried to use some malicious JavaScript to steal your bank information and your bank allowed API calls from anywhere, this could be a bad news bears™️ situation.
- Let's say your bank is hosted on
https://bankofamerica.com
but a clever hacker tried to impersonate you and send a request with an origin ofhttps://couponvirus.org
. Ideally, your bank would reject requests from unknown origins such ascouponvirus.org
and only allow requests from trusted origins or domains likebankofamerica.com
- Navigate to your backend, in your Gemfile uncomment
gem 'rack-cors'
This will allow us to setup Cross Origin Resource Sharing (CORS) in our API. - Run
bundle
. - Inside of
config/initializers/cors.rb
uncomment the following code:
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins '*'
resource '*',
headers: :any,
methods: [:get, :post, :put, :patch, :delete, :options, :head]
end
end
(This snippet is from the documentation for the rack-cors gem.)
Inside the allow
block, origins '*'
means we are allowing requests from all origins and are allowing [:get, :post, :patch, :delete]
requests to the API. Read this if you need a refresher on HTTP methods.
This may come as a shock but the config.api_only = true
option found in config/application.rb
tells our app that it is going to be an API only. In other words, our API will not generate any HTML and instead will return JSON. The frontend is responsible for taking that JSON, formatting the data, and generating HTML to show to the user. Read this if you want to review what JSON is and why we use it.
For now, we will leave the origins open. Later on, we can change this to only allow requests from the address of the frontend repo––localhost:8000 or www.myapp.com
for example.
Now let's fetch the syllabi (from our Rails API index route, Ex: 'http://localhost:3000/api/v1/syllabuses')
Let's save ourselves trouble in the future and add this to the top of our index.js
const endPoint = <YOUR_ENDPOINT>
/* index.js */
const endPoint = "http://localhost:3000/api/v1/beaches";
document.addEventListener('DOMContentLoaded', () => {
getBeaches()
});
function getBeaches() {
fetch(endPoint)
.then(res => res.json())
.then(json => console.log(json));
}
If you see the notes printed to the console, you're good to move forward.
The next step is getting the beaches added to the DOM. No problem, add an empty <div>
or <ul>
element to index.html
and go ahead and add each beach title, along with an edit button. We'll give the button a data-id
in case we want to implement edit functionality in the future.
/* src/index.js */
document.addEventListener('DOMContentLoaded', () => {
const endPoint = "http://localhost:3000/api/v1/beaches"
getBeaches()
})
function getBeaches() {
fetch(endPoint)
.then(res => res.json())
.then(beaches => {
// remember our JSON data is a bit nested due to our serializer
beaches.data.forEach(beach => {
// double check how your data is nested in the console so you can successfully access the attributes of each individual object
const beachMarkup = `
<div data-id=${beach.id}>
<img src=${beach.attributes.image_url} height="200" width="250">
<h3>${beach.attributes.title}</h3>
<p>${beach.attributes.category.name}</p>
<button data-id=${beach.id}>edit</button>
</div>
<br><br>`;
document.querySelector('#beach-container').innerHTML += beachMarkup
})
})
}
<img src=${beaches.attributes.image_url} height="200" width="250">
There are many ways to do this. The above snippet is not super pretty, but it works.
Source: JavaScript Rails API Project Setup
Now that we've made a GET request we're familiar with how to GET data from the server but what about SENDING data to the server? Let's walk through it!
The key differences are in order to POST data to the server, you might need to get some input from the user. To collect this we're going to use a HTML form.
<div class="form-container">
<form id="create-beach-form">
<h3>Create a Beach!</h3>
<input id='input-name' type="text" name="name" value="" placeholder="Enter your beach name..." class="input-text">
<br><br>
<p>Select a Country</p>
<select id="countries" name="countries">
<option value="1">Greece</option>
<option value="2">Spain</option>
<option value="3">France</option>
</select>
<br><br>
<input id='input-location' type="text" name="location" value="" placeholder="Enter your beach location..." class="input-text">
<br><br>
<textarea id='input-description' name="description" rows="8" cols="80" value="" placeholder="Enter your beach description..."></textarea>
<br><br>
<input id='input-url' type="text" name="image" value="" placeholder="Enter your beach image URL..." class="input-text">
<br><br>
<input id= 'create-button' type="submit" name="submit" value="Create New Beach" class="submit">
</form>
<br><br>
</div>
We'll use this form to allow a visitor to the site to create a beach using our app.
In the DOMContentLoaded event listener we can find the form on the DOM and attach a submit event listener to the form element.
document.addEventListener("DOMContentLoaded", () => {
getBeaches()
let createBeachForm = document.querySelector('#create-beach-form')
createBeachForm.addEventListener('submit', (event) => createFormHandler(event))
})
Gather all the input values and pass it to your function to execute the post fetch.
function createFormHandler(event) {
event.preventDefault()
const nameInput = document.querySelector('#input-name').value
const countryId = parseInt(document.querySelector('#countries').value)
const locationInput = document.querySelector('#input-location').value
const descriptionInput = document.querySelector('#input-description').value
const imageInput = document.querySelector('#input-url').value
postBeach(nameInput, countryId, locationInput, descriptionInput, imageInput)
}
function postBeach(name, country_id, location, description, image_url) {
// console.log(name, country_id, location, description, image_url);
let bodyData = {name, country_id, location, description, image_url}
fetch(endPoint, {
// POST request
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify(bodyData)
})
.then(response => response.json())
.then(beach => {
console.log(beach)
const beachData = beach.data
// render JSON response, render data to user to see what created, manuplate DOM by showing user what created, data is pointing to single object not array like in get fetch where I had arrey and .forEach
const beachMarkup = `
<div data-id=${beach.id}>
<h3>${beachData.attributes.name}</h3>
<p>${beachData.attributes.country.name}</p>
<p>${beachData.attributes.location}</p>
<p>${beachData.attributes.description}</p>
<img src=${beachData.attributes.image_url} height="200" width="250">
<button data-id=${beachData.id}>edit</button>
</div>
<br><br>`;
// I am selecting container and updaing HTML
document.querySelector('#beach-container').innerHTML += beachMarkup;
})
}
Refactor your code to make it more DRY and implement a render function you can reuse in multiple places.
function render(beach) {
const beachMarkup = `
<div data-id=${beach.id}>
<h3>${beach.attributes.name}</h3>
<p>${beach.attributes.country.name}</p>
<p>${beach.attributes.location}</p>
<p>${beach.attributes.description}</p>
<img src=${beach.attributes.image_url} height="200" width="250">
<br><br>
<button data-id=${beach.id}>edit</button>
</div>
<br><br>`;
document.querySelector('#beach-container').innerHTML += beachMarkup
}
- Implement
.catch()
in fetch requests to handle errors.