How to build a full-stack serverless movie tracker with OpenJS Architect - Part 2
Paul Chin Jr.
Posted on May 6, 2021
Progressive Enhancement and delivering client-side JS
The basic functionality is completed. Now it's time to enhance this form with some JavaScript so it saves the watched checkboxes in real-time by submitting the form when a checkbox is clicked.
// public/index.js
let forms = document.querySelectorAll("form[action='/watched']")
for (let f of forms) {
f.querySelector('button').style.display = 'none'
let check = f.querySelector('input[type="checkbox"]')
check.addEventListener('change', function (e) {
f.submit()
}, false)
}
This is the simplest version of progressive enhancement we can make. The client-side JavavScript selects the forms, hides the 'save' button and adds an event listener to the checkbox. Each time the checkbox changes, it submits the form for the user.
Adding star ratings and comments
We're going to continue making our application more complex. In the next few sections, we will add a "star rating" feature as well as a comment system.
First, lets change our markup in the get-index
route.
// src/http/get-index/index.js
const arc = require('@architect/functions')
const data = require('@begin/data')
exports.handler = arc.http.async(http)
function authControl(account) {
if (account && account.name) {
return `
Welcome back ${account.name}
<form action=/logout method="post">
<button>Logout</button>
</form>`
} else {
let clientID = process.env.GITHUB_CLIENT_ID
let redirectURL = process.env.GITHUB_REDIRECT
let href = `https://github.com/login/oauth/authorize?client_id=${clientID}&redirect_url=${redirectURL}`
return `
<a href='${href}'>Login with GitHub to see a list of movies</a>
`
}
}
function movie({ key, watched, title, rating, review }) {
return `
<form action="/watched" method="post">
<input type="hidden" name="movieId" value="${key}">
<input type="checkbox" data-movieid="${key}" name=watched ${watched ? 'checked' : ''}>
${title}
<input type="text" data-movieid="${key}" name="review" placeholder="leave a review here" value="${review || ''}">
<input type="radio" name="rating" data-movieid="${key}" value="1" ${rating === '1' ? 'checked' : ''}>
<input type="radio" name="rating" data-movieid="${key}" value="2" ${rating === '2' ? 'checked' : ''}>
<input type="radio" name="rating" data-movieid="${key}" value="3" ${rating === '3' ? 'checked' : ''}>
<button class=cage>Save</button>
</form>`
}
async function getMovies(account) {
let movies = [
{ key: '001', title: 'Raising Arizona' },
{ key: '002', title: 'Con Air' },
{ key: '003', title: 'National Treasure' },
]
if (account) {
let accountMovies = await data.get({
table: `${account.id}-movies`
})
let result = ''
for (let mov of movies) {
let found = (accountMovies.find(m => m.key === mov.key))
result += movie({
key: mov.key,
title: mov.title,
watched: !!found,
rating: found ? found.rating : '',
review: found ? found.review : ''
})
}
return result
}
return ''
}
async function http(req) {
return {
html: `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/_static/index.css">
<link rel="shortcut icon" href="#">
<title>Praise Cage</title>
</head>
<body>
<h1>Praise Cage</h1>
${authControl(req.session.account)}
${await getMovies(req.session.account)}
<script src=/_static/index.js type=module></script>
</body>
</html>
`
}
}
Next, we can modify the post-watched
endpoint. We now want to include review
and rating
properties to be saved to the database.
// src/http/post-watched/index.js
const arc = require('@architect/functions')
const data = require('@begin/data')
exports.handler = arc.http.async(route)
async function route(req) {
console.log('post-watched req.body:', req.body )
let account = req.session.account.id
if (req.body.watched) {
await data.set({
table: `${account}-movies`,
key: req.body.movieId,
review: req.body.review,
rating: req.body.rating
})
} else {
await data.destroy({
table: `${account}-movies`,
key: req.body.movieId
})
}
return {
location: '/'
}
}
Finally, we can update our client-side JS with the following:
// public/index.js
let forms = document.querySelectorAll("form[action='/watched']")
console.log(forms)
for (let f of forms) {
// hide all submit buttons
f.querySelector('button').style.display = 'none'
// get a ref to the form checkbox
let check = f.querySelector('input[type="checkbox"]')
// get a ref to the form radios
let radios = f.querySelectorAll('input[type="radio"]')
// get a ref to the form text
let text = f.querySelector('input[type="text"]')
// mutates state
function changed (e) {
let movieId = e.target.dataset.movieid
let payload = { movieId }
payload.watched = f.querySelector('input[name="watched"]').checked
payload.review = f.querySelectorAll('input[name="review"]')[0].value
let rating = f.querySelectorAll('input[name="rating"]:checked')
payload.rating = rating.length === 1 ? rating[0].value : ''
//make an HTTP post with fetch
fetch('/watched', {
method: 'POST',
headers: {
'content-type': 'application/json',
'x-nick-cage': 'fetch'
},
body: JSON.stringify(payload)
}).catch(function fail(err) {
console.log('failed', err)
})
}
// listen to checkbox changes
check.addEventListener('change', changed, false)
// listen to radio buttons getting hit
for (let r of radios) {
r.addEventListener('input', changed, false)
}
// listen to changes to review text
text.addEventListener('input', changed, false)
}
The full source code can be found here: https://github.com/pchinjr/movie-tracker
Posted on May 6, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.