Dave
Posted on June 30, 2022
Let's wrap things up.
In folder node-server edit note.model.js to:
const { prisma } = require("./db")
async function getNotes() {
return prisma.note.findMany()
}
async function getNote(id) {
return prisma.note.findUnique({ where: { id } })
}
async function createNote(
note
) {
return prisma.note.create({
data: note
})
}
async function updateNote(
id, note
) {
return prisma.note.update({
data: note,
where: {
id
}
})
}
async function deleteNote(
id
) {
return prisma.note.delete({
where: {
id
}
})
}
module.exports = {
getNotes,
getNote,
createNote,
updateNote,
deleteNote,
}
In folder node-server edit note.controller.js to:
const authorRepo = require('../models/author.model');
const noteRepo = require('../models/note.model');
async function getNotes(req, res) {
const notes = await noteRepo.getNotes();
res.json({
notes
});
}
async function getNote(req, res) {
const {id} = req.params;
const note = await noteRepo.getNote(id);
const { authorId, ...noteRest } = note;
const { username } = await authorRepo.getAuthor(authorId);
res.json({ note: {
...noteRest,
author: username
}
});
}
async function retrieveOrCreateAuthor(username) {
let author = await authorRepo.getAuthorByName(username);
if (author === null) {
author = await authorRepo.createAuthor({
username
})
}
return author
}
async function postNote(req, res) {
const {body} = req;
const {title, content, author, lang, isLive, category} = body;
try {
const noteAuthor = await retrieveOrCreateAuthor(author);
const note = await noteRepo.createNote({
title,
content,
lang,
isLive,
category,
authorId: noteAuthor.id
})
res
.status(200)
.json({
note
})
} catch (e) {
console.error(e);
res.status(500).json({error: "Something went wrong"})
}
}
async function putNote(req, res) {
const {body} = req;
const {id, title, content, author, lang, isLive, category} = body;
try {
const noteAuthor = await retrieveOrCreateAuthor(author);
const note = await noteRepo.updateNote(id, {
title,
content,
lang,
isLive,
category,
authorId: noteAuthor.id
})
res
.status(200)
.json({
note
})
} catch (e) {
console.error(e);
res.status(500).json({error: "Something went wrong"})
}
}
async function deleteNote(req, res) {
const {body} = req;
const {id} = body;
try {
await noteRepo.deleteNote(id)
res
.status(200).send()
} catch (e) {
console.error(e);
res.status(500).json({error: "Something went wrong"})
}
}
module.exports = {
getNotes,
getNote,
postNote,
putNote,
deleteNote,
}
In node-server edit routes/index.js to:
const express = require('express');
const noteRouter = express.Router();
const noteController = require('../controllers/note.controller');
noteRouter.get('/', noteController.getNotes);
noteRouter.get('/:id', noteController.getNote);
noteRouter.post('/', noteController.postNote);
noteRouter.put('/', noteController.putNote);
noteRouter.delete('/', noteController.deleteNote);
const routes = app => {
app.use('/note', noteRouter);
};
module.exports = routes
Server side we now have all the operations we need for the basic CRUD operations.
Create, Read, Update, Delete
Try running the client and server now. If you click the submit button on the form you'll notice two problems: first the form doesn't respond, you could click over and over and not know if anything's happened. Second, if you look at the server console you'll notice an error.
Argument isLive: Got invalid value 'true' on prisma.createOneNote. Provided String, expected Boolean.
isLive
is a boolean but is being sent to Prisma as a string.
In node-server index.js we are using:
app.use(bodyParser.json());
This does indeed retrieve the correct types during parsing, so the problem must be in the client. When we gather up the input control data in Form.js in the onSubmit
handler we are using input.value
which always returns a string.
Edit Form.js to:
import React, {useState} from 'react';
import InputLabel from "./InputLabel";
import {isEmptyString, isNullOrUndefined, titleFromName} from "./strings";
import './form.css'
const Form = ({entity, onSubmitHandler, onDeleteHandler}) => {
const [isSubmitting, setIsSubmitting] = useState(false);
return (
<form onSubmit={e => {
setIsSubmitting(true);
const form = e.target;
const newEntity = Object.values(form).reduce((obj, field) => {
const {name} = field;
if (!isEmptyString(name)) {
switch (typeof entity[name]) {
case "number":
obj[name] = field.valueAsNumber;
break;
case "boolean":
obj[name] = field.value === 'true';
break;
default:
obj[name] = field.value
}
}
return obj
}, {})
onSubmitHandler(newEntity);
e.stopPropagation();
e.preventDefault()
}}>
<fieldset
disabled={isSubmitting}
>
{
Object.entries(entity).map(([entityKey, entityValue]) => {
if (entityKey === "id") {
return <input
type="hidden"
name="id"
key="id"
value={entityValue}
/>
} else {
return <InputLabel
id={entityKey}
key={entityKey}
label={titleFromName(entityKey)}
type={
typeof entityValue === "boolean"
? "checkbox"
: "text"
}
value={entityValue}
/>
}
})
}
</fieldset>
<button
type="submit"
disabled={isSubmitting}
>
{
isSubmitting ? 'Submitting' : 'Submit'
}
</button>
{
onDeleteHandler && !isNullOrUndefined(entity.id) && <button
disabled={isSubmitting}
onClick={() => {
setIsSubmitting(true);
onDeleteHandler(entity.id)
}}
>
Delete
</button>
}
</form>
);
};
export default Form;
Changes:
- We wrap our input controls with a fieldset tag, allowing us to disable all controls when the user clicks "Submit"
- We use a switch statement to parse the input value so it matches the type of the original entity we use to build the form.
If you try saving a form again you'll notice the bug is fixed.
Before we implement the rest of the CRUD operations a small refactor is needed. In react-client, create .env.development
REACT_APP_URL_API=http://localhost:4011/
Create useFetch.js:
import {useState, useEffect} from "react";
export const getUrl = url => new URL(url, process.env.REACT_APP_URL_API).toString();
function useFetch(url, skip) {
const [data, setData] = useState({});
useEffect( () => {
const abortController = new AbortController();
async function fetchData() {
const fullUrl = getUrl(url);
console.log('Fetching from: ' + fullUrl);
try {
const response = await fetch(fullUrl, {
signal: abortController.signal,
});
if (response.ok) {
console.log('Response received from server and is ok!')
const res = await response.json();
if (abortController.signal.aborted) {
console.log('Abort detected, exiting!')
return;
}
setData(res)
}
} catch(e) {
console.log(e)
}
}
!skip && fetchData()
return () => {
console.log('Aborting GET request.')
abortController.abort();
}
}, [url, setData, skip])
return data
}
export default useFetch
Currently our form can only add new notes, not edit. We need to do a few things:
- List all notes
- Edit a note
- Add a note
- Delete a note
Refactor AddEditNote.js to:
import React from 'react';
import {useParams, useNavigate} from "react-router-dom";
import RenderData from "./RenderData";
import Form from './Form';
import useFetch, {getUrl} from "./useFetch";
import {isNullOrUndefined} from "./strings";
const AddEditNote = () => {
const {noteId} = useParams();
const {note = {
title: '',
content: '',
lang: '',
isLive: false,
category: '',
author: '',
}} = useFetch('note/' + noteId, isNullOrUndefined(noteId));
const navigate = useNavigate();
return (
<div>
<RenderData
data={note}
/>
<Form
entity={note}
onSubmitHandler={async newNote => {
console.log({newNote})
const response = await fetch(getUrl('note'), {
method: isNullOrUndefined(newNote.id) ? 'POST' : 'PUT',
body: JSON.stringify(newNote),
headers: {
'Content-Type': 'application/json'
}
});
if (response.ok) {
await response.json()
navigate('/note-list')
}
}}
onDeleteHandler={async (id) => {
if (!isNullOrUndefined(id)) {
await fetch(getUrl('note'), {
method: 'DELETE',
body: JSON.stringify({id}),
headers: {
'Content-Type': 'application/json'
}
});
navigate('/note-list')
}
}}
/>
</div>
);
};
export default AddEditNote;
In react-client Create TableList.js
import React from 'react';
import {titleFromName} from './strings';
import './table-list.css';
const TableList = ({
data,
title,
onClickHandler,
idField = 'id',
fieldFormatter = {},
}) => {
if (!data || data.length === 0) {
return null
}
const firstRow = data[0];
const dataColumnNamesToRender = Object.getOwnPropertyNames(firstRow)
.filter(propName => propName !== idField);
const headerRow = dataColumnNamesToRender.map((propName, i) => <th
key={i}
>
{
titleFromName(propName)
}
</th>);
return (
<table>
<caption>
{
title
}
</caption>
<thead>
<tr>
{
headerRow
}
</tr>
</thead>
<tbody>
{
data.map((dataRow, i) => <tr
key={i}
onClick={() => onClickHandler && onClickHandler(dataRow[idField])}
>
{
dataColumnNamesToRender.map((dataColumnName, i) => <td
key={i}
>
{
(fieldFormatter[dataColumnName] ?? (v => v))(dataRow[dataColumnName], dataRow)
}
</td>)
}
</tr>)
}
</tbody>
</table>
);
};
export default TableList;
In react-client Create table-list.css
table {
margin: 12px;
border-collapse: collapse;
}
th {
color: white;
padding: 8px;
background-color: #444;
}
td {
border-bottom: 1px solid #ddd;
padding: 12px;
}
td a,
td a:visited {
color: black;
}
td:not(:last-child) {
border-left:1px solid #ccc;
border-right: 1px solid #ccc;
}
tr:nth-child(even) {
background-color: #f1f1f1;
}
Just like our generic Form component, this is a generic data list component.
In react-client Create NoteList.js
import React from 'react';
import TableList from "./TableList";
import {Link} from "react-router-dom";
import useFetch from "./useFetch";
const NoteList = () => {
const {notes} = useFetch('note')
return (
<TableList
data={notes}
fieldFormatter={{
title: (title, dataRow) => [
<Link
to={`/edit-note/${dataRow.id}`}
key='1'
>
edit
</Link>,
<span key="2">
{
title
}
</span>
],
dateCreated: date => new Date(date).toLocaleString()
}}
/>
);
};
export default NoteList;
This uses TableList.js to list out Notes.
Finally, change App.js to:
import {
Link,
HashRouter as Router,
Routes,
Route,
} from 'react-router-dom';
import AddEditNote from "./AddEditNote";
import NoteList from "./NoteList";
import './App.css';
function App() {
return (
<div className="App">
<Router>
<Routes>
<Route exact path="/" element={
<ul>
<li>
<Link to="/note-list">List Notes</Link>
</li>
<li>
<Link to="/edit-note">Create Note</Link>
</li>
</ul>
}/>
<Route path="/note-list" element={<NoteList/>}/>
<Route path="/edit-note" element={<AddEditNote/>}/>
<Route path="/edit-note/:noteId" element={<AddEditNote/>}/>
</Routes>
</Router>
</div>
);
}
export default App;
If you run this now, you have all basic CRUD operations working.
Congrats, full-stack.
This app is missing a few things: form validation and date handling, also dropdown lists; however, these should be easy things to add...
Code repo: Github Repository
Posted on June 30, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.