simply learn-full-stack-8
November 21, 2022
Simply Learn Full-Stack React & Node.js
Let's wrap things up.
In folder node-server edit note.model.js to:
1const { prisma } = require("./db")23async function getNotes() {4 return prisma.note.findMany()5}67async function getNote(id) {8 return prisma.note.findUnique({ where: { id } })9}1011async function createNote(12 note13) {14 return prisma.note.create({15 data: note16 })17}1819async function updateNote(20 id, note21) {22 return prisma.note.update({23 data: note,24 where: {25 id26 }27 })28}2930async function deleteNote(31 id32) {33 return prisma.note.delete({34 where: {35 id36 }37 })38}3940module.exports = {41 getNotes,42 getNote,43 createNote,44 updateNote,45 deleteNote,46}
In folder node-server edit note.controller.js to:
1const authorRepo = require('../models/author.model');2const noteRepo = require('../models/note.model');34async function getNotes(req, res) {5 const notes = await noteRepo.getNotes();67 res.json({8 notes9 });10}1112async function getNote(req, res) {13 const {id} = req.params;14 const note = await noteRepo.getNote(id);15 const { authorId, ...noteRest } = note;16 const { username } = await authorRepo.getAuthor(authorId);1718 res.json({ note: {19 ...noteRest,20 author: username21 }22 });23}2425async function retrieveOrCreateAuthor(username) {26 let author = await authorRepo.getAuthorByName(username);27 if (author === null) {28 author = await authorRepo.createAuthor({29 username30 })31 }3233 return author34}3536async function postNote(req, res) {37 const {body} = req;38 const {title, content, author, lang, isLive, category} = body;3940 try {41 const noteAuthor = await retrieveOrCreateAuthor(author);4243 const note = await noteRepo.createNote({44 title,45 content,46 lang,47 isLive,48 category,49 authorId: noteAuthor.id50 })5152 res53 .status(200)54 .json({55 note56 })57 } catch (e) {58 console.error(e);59 res.status(500).json({error: "Something went wrong"})60 }61}6263async function putNote(req, res) {64 const {body} = req;65 const {id, title, content, author, lang, isLive, category} = body;6667 try {68 const noteAuthor = await retrieveOrCreateAuthor(author);69 const note = await noteRepo.updateNote(id, {70 title,71 content,72 lang,73 isLive,74 category,75 authorId: noteAuthor.id76 })7778 res79 .status(200)80 .json({81 note82 })83 } catch (e) {84 console.error(e);85 res.status(500).json({error: "Something went wrong"})86 }87}8889async function deleteNote(req, res) {90 const {body} = req;91 const {id} = body;9293 try {94 await noteRepo.deleteNote(id)9596 res97 .status(200).send()98 } catch (e) {99 console.error(e);100 res.status(500).json({error: "Something went wrong"})101 }102}103104module.exports = {105 getNotes,106 getNote,107 postNote,108 putNote,109 deleteNote,110}
In node-server edit routes/index.js to:
1const express = require('express');2const noteRouter = express.Router();3const noteController = require('../controllers/note.controller');45noteRouter.get('/', noteController.getNotes);6noteRouter.get('/:id', noteController.getNote);7noteRouter.post('/', noteController.postNote);8noteRouter.put('/', noteController.putNote);9noteRouter.delete('/', noteController.deleteNote);1011const routes = app => {12 app.use('/note', noteRouter);13};1415module.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:
1import React, {useState} from 'react';2import InputLabel from "./InputLabel";3import {isEmptyString, isNullOrUndefined, titleFromName} from "./strings";4import './form.css'56const Form = ({entity, onSubmitHandler, onDeleteHandler}) => {7 const [isSubmitting, setIsSubmitting] = useState(false);89 return (10 <form onSubmit={e => {11 setIsSubmitting(true);12 const form = e.target;13 const newEntity = Object.values(form).reduce((obj, field) => {14 const {name} = field;1516 if (!isEmptyString(name)) {17 switch (typeof entity[name]) {18 case "number":19 obj[name] = field.valueAsNumber;20 break;21 case "boolean":22 obj[name] = field.value === 'true';23 break;24 default:25 obj[name] = field.value26 }27 }2829 return obj30 }, {})31 onSubmitHandler(newEntity);3233 e.stopPropagation();34 e.preventDefault()35 }}>36 <fieldset37 disabled={isSubmitting}38 >39 {40 Object.entries(entity).map(([entityKey, entityValue]) => {41 if (entityKey === "id") {42 return <input43 type="hidden"44 name="id"45 key="id"46 value={entityValue}47 />48 } else {49 return <InputLabel50 id={entityKey}51 key={entityKey}52 label={titleFromName(entityKey)}53 type={54 typeof entityValue === "boolean"55 ? "checkbox"56 : "text"57 }58 value={entityValue}59 />60 }61 })62 }63 </fieldset>64 <button65 type="submit"66 disabled={isSubmitting}67 >68 {69 isSubmitting ? 'Submitting' : 'Submit'70 }71 </button>72 {73 onDeleteHandler && !isNullOrUndefined(entity.id) && <button74 disabled={isSubmitting}75 onClick={() => {76 setIsSubmitting(true);77 onDeleteHandler(entity.id)78 }}79 >80 Delete81 </button>82 }83 </form>84 );85};8687export 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:
1import {useState, useEffect} from "react";23export const getUrl = url => new URL(url, process.env.REACT_APP_URL_API).toString();45function useFetch(url, skip) {6 const [data, setData] = useState({});78 useEffect( () => {9 const abortController = new AbortController();1011 async function fetchData() {12 const fullUrl = getUrl(url);13 console.log('Fetching from: ' + fullUrl);14 try {15 const response = await fetch(fullUrl, {16 signal: abortController.signal,17 });1819 if (response.ok) {20 console.log('Response received from server and is ok!')21 const res = await response.json();2223 if (abortController.signal.aborted) {24 console.log('Abort detected, exiting!')25 return;26 }2728 setData(res)29 }30 } catch(e) {31 console.log(e)32 }33 }3435 !skip && fetchData()3637 return () => {38 console.log('Aborting GET request.')39 abortController.abort();40 }41 }, [url, setData, skip])4243 return data44}4546export 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:
1import React from 'react';2import {useParams, useNavigate} from "react-router-dom";3import RenderData from "./RenderData";4import Form from './Form';5import useFetch, {getUrl} from "./useFetch";6import {isNullOrUndefined} from "./strings";78const AddEditNote = () => {9 const {noteId} = useParams();10 const {note = {11 title: '',12 content: '',13 lang: '',14 isLive: false,15 category: '',16 author: '',17 }} = useFetch('note/' + noteId, isNullOrUndefined(noteId));18 const navigate = useNavigate();1920 return (21 <div>22 <RenderData23 data={note}24 />25 <Form26 entity={note}27 onSubmitHandler={async newNote => {28 console.log({newNote})29 const response = await fetch(getUrl('note'), {30 method: isNullOrUndefined(newNote.id) ? 'POST' : 'PUT',31 body: JSON.stringify(newNote),32 headers: {33 'Content-Type': 'application/json'34 }35 });3637 if (response.ok) {38 await response.json()39 navigate('/note-list')40 }41 }}42 onDeleteHandler={async (id) => {43 if (!isNullOrUndefined(id)) {44 await fetch(getUrl('note'), {45 method: 'DELETE',46 body: JSON.stringify({id}),47 headers: {48 'Content-Type': 'application/json'49 }50 });5152 navigate('/note-list')53 }54 }}55 />56 </div>57 );58};5960export default AddEditNote;
In react-client Create TableList.js
1import React from 'react';2import {titleFromName} from './strings';3import './table-list.css';45const TableList = ({6 data,7 title,8 onClickHandler,9 idField = 'id',10 fieldFormatter = {},11 }) => {12 if (!data || data.length === 0) {13 return null14 }15 const firstRow = data[0];16 const dataColumnNamesToRender = Object.getOwnPropertyNames(firstRow)17 .filter(propName => propName !== idField);1819 const headerRow = dataColumnNamesToRender.map((propName, i) => <th20 key={i}21 >22 {23 titleFromName(propName)24 }25 </th>);2627 return (28 <table>29 <caption>30 {31 title32 }33 </caption>34 <thead>35 <tr>36 {37 headerRow38 }39 </tr>40 </thead>41 <tbody>42 {43 data.map((dataRow, i) => <tr44 key={i}45 onClick={() => onClickHandler && onClickHandler(dataRow[idField])}46 >47 {48 dataColumnNamesToRender.map((dataColumnName, i) => <td49 key={i}50 >51 {52 (fieldFormatter[dataColumnName] ?? (v => v))(dataRow[dataColumnName], dataRow)53 }54 </td>)55 }56 </tr>)57 }58 </tbody>59 </table>60 );61};6263export default TableList;
In react-client Create table-list.css
1table {2 margin: 12px;3 border-collapse: collapse;4}56th {7 color: white;8 padding: 8px;9 background-color: #444;10}1112td {13 border-bottom: 1px solid #ddd;14 padding: 12px;15}1617td a,18td a:visited {19 color: black;20}2122td:not(:last-child) {23 border-left:1px solid #ccc;24 border-right: 1px solid #ccc;25}2627tr:nth-child(even) {28 background-color: #f1f1f1;29}
Just like our generic Form component, this is a generic data list component.
In react-client Create NoteList.js
1import React from 'react';2import TableList from "./TableList";3import {Link} from "react-router-dom";4import useFetch from "./useFetch";56const NoteList = () => {7 const {notes} = useFetch('note')89 return (10 <TableList11 data={notes}12 fieldFormatter={{13 title: (title, dataRow) => [14 <Link15 to={`/edit-note/${dataRow.id}`}16 key='1'17 >18 edit19 </Link>,20 <span key="2">21 {22 title23 }24 </span>25 ],26 dateCreated: date => new Date(date).toLocaleString()27 }}28 />29 );30};3132export default NoteList;
This uses TableList.js to list out Notes.
Finally, change App.js to:
1import {2 Link,3 HashRouter as Router,4 Routes,5 Route,6} from 'react-router-dom';7import AddEditNote from "./AddEditNote";8import NoteList from "./NoteList";9import './App.css';1011function App() {12 return (13 <div className="App">14 <Router>15 <Routes>16 <Route exact path="/" element={17 <ul>18 <li>19 <Link to="/note-list">List Notes</Link>20 </li>21 <li>22 <Link to="/edit-note">Create Note</Link>23 </li>24 </ul>25 }/>26 <Route path="/note-list" element={<NoteList/>}/>27 <Route path="/edit-note" element={<AddEditNote/>}/>28 <Route path="/edit-note/:noteId" element={<AddEditNote/>}/>29 </Routes>30 </Router>31 </div>32 );33}3435export 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