This site runs best with JavaScript enabled.

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")
2
3async function getNotes() {
4 return prisma.note.findMany()
5}
6
7async function getNote(id) {
8 return prisma.note.findUnique({ where: { id } })
9}
10
11async function createNote(
12 note
13) {
14 return prisma.note.create({
15 data: note
16 })
17}
18
19async function updateNote(
20 id, note
21) {
22 return prisma.note.update({
23 data: note,
24 where: {
25 id
26 }
27 })
28}
29
30async function deleteNote(
31 id
32) {
33 return prisma.note.delete({
34 where: {
35 id
36 }
37 })
38}
39
40module.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');
3
4async function getNotes(req, res) {
5 const notes = await noteRepo.getNotes();
6
7 res.json({
8 notes
9 });
10}
11
12async 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);
17
18 res.json({ note: {
19 ...noteRest,
20 author: username
21 }
22 });
23}
24
25async function retrieveOrCreateAuthor(username) {
26 let author = await authorRepo.getAuthorByName(username);
27 if (author === null) {
28 author = await authorRepo.createAuthor({
29 username
30 })
31 }
32
33 return author
34}
35
36async function postNote(req, res) {
37 const {body} = req;
38 const {title, content, author, lang, isLive, category} = body;
39
40 try {
41 const noteAuthor = await retrieveOrCreateAuthor(author);
42
43 const note = await noteRepo.createNote({
44 title,
45 content,
46 lang,
47 isLive,
48 category,
49 authorId: noteAuthor.id
50 })
51
52 res
53 .status(200)
54 .json({
55 note
56 })
57 } catch (e) {
58 console.error(e);
59 res.status(500).json({error: "Something went wrong"})
60 }
61}
62
63async function putNote(req, res) {
64 const {body} = req;
65 const {id, title, content, author, lang, isLive, category} = body;
66
67 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.id
76 })
77
78 res
79 .status(200)
80 .json({
81 note
82 })
83 } catch (e) {
84 console.error(e);
85 res.status(500).json({error: "Something went wrong"})
86 }
87}
88
89async function deleteNote(req, res) {
90 const {body} = req;
91 const {id} = body;
92
93 try {
94 await noteRepo.deleteNote(id)
95
96 res
97 .status(200).send()
98 } catch (e) {
99 console.error(e);
100 res.status(500).json({error: "Something went wrong"})
101 }
102}
103
104module.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');
4
5noteRouter.get('/', noteController.getNotes);
6noteRouter.get('/:id', noteController.getNote);
7noteRouter.post('/', noteController.postNote);
8noteRouter.put('/', noteController.putNote);
9noteRouter.delete('/', noteController.deleteNote);
10
11const routes = app => {
12 app.use('/note', noteRouter);
13};
14
15module.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'
5
6const Form = ({entity, onSubmitHandler, onDeleteHandler}) => {
7 const [isSubmitting, setIsSubmitting] = useState(false);
8
9 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;
15
16 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.value
26 }
27 }
28
29 return obj
30 }, {})
31 onSubmitHandler(newEntity);
32
33 e.stopPropagation();
34 e.preventDefault()
35 }}>
36 <fieldset
37 disabled={isSubmitting}
38 >
39 {
40 Object.entries(entity).map(([entityKey, entityValue]) => {
41 if (entityKey === "id") {
42 return <input
43 type="hidden"
44 name="id"
45 key="id"
46 value={entityValue}
47 />
48 } else {
49 return <InputLabel
50 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 <button
65 type="submit"
66 disabled={isSubmitting}
67 >
68 {
69 isSubmitting ? 'Submitting' : 'Submit'
70 }
71 </button>
72 {
73 onDeleteHandler && !isNullOrUndefined(entity.id) && <button
74 disabled={isSubmitting}
75 onClick={() => {
76 setIsSubmitting(true);
77 onDeleteHandler(entity.id)
78 }}
79 >
80 Delete
81 </button>
82 }
83 </form>
84 );
85};
86
87export default Form;

Changes:

  1. We wrap our input controls with a fieldset tag, allowing us to disable all controls when the user clicks "Submit"
  2. 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";
2
3export const getUrl = url => new URL(url, process.env.REACT_APP_URL_API).toString();
4
5function useFetch(url, skip) {
6 const [data, setData] = useState({});
7
8 useEffect( () => {
9 const abortController = new AbortController();
10
11 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 });
18
19 if (response.ok) {
20 console.log('Response received from server and is ok!')
21 const res = await response.json();
22
23 if (abortController.signal.aborted) {
24 console.log('Abort detected, exiting!')
25 return;
26 }
27
28 setData(res)
29 }
30 } catch(e) {
31 console.log(e)
32 }
33 }
34
35 !skip && fetchData()
36
37 return () => {
38 console.log('Aborting GET request.')
39 abortController.abort();
40 }
41 }, [url, setData, skip])
42
43 return data
44}
45
46export default useFetch

Currently our form can only add new notes, not edit. We need to do a few things:

  1. List all notes
  2. Edit a note
  3. Add a note
  4. 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";
7
8const 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();
19
20 return (
21 <div>
22 <RenderData
23 data={note}
24 />
25 <Form
26 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 });
36
37 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 });
51
52 navigate('/note-list')
53 }
54 }}
55 />
56 </div>
57 );
58};
59
60export default AddEditNote;

In react-client Create TableList.js

1import React from 'react';
2import {titleFromName} from './strings';
3import './table-list.css';
4
5const TableList = ({
6 data,
7 title,
8 onClickHandler,
9 idField = 'id',
10 fieldFormatter = {},
11 }) => {
12 if (!data || data.length === 0) {
13 return null
14 }
15 const firstRow = data[0];
16 const dataColumnNamesToRender = Object.getOwnPropertyNames(firstRow)
17 .filter(propName => propName !== idField);
18
19 const headerRow = dataColumnNamesToRender.map((propName, i) => <th
20 key={i}
21 >
22 {
23 titleFromName(propName)
24 }
25 </th>);
26
27 return (
28 <table>
29 <caption>
30 {
31 title
32 }
33 </caption>
34 <thead>
35 <tr>
36 {
37 headerRow
38 }
39 </tr>
40 </thead>
41 <tbody>
42 {
43 data.map((dataRow, i) => <tr
44 key={i}
45 onClick={() => onClickHandler && onClickHandler(dataRow[idField])}
46 >
47 {
48 dataColumnNamesToRender.map((dataColumnName, i) => <td
49 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};
62
63export default TableList;

In react-client Create table-list.css

1table {
2 margin: 12px;
3 border-collapse: collapse;
4}
5
6th {
7 color: white;
8 padding: 8px;
9 background-color: #444;
10}
11
12td {
13 border-bottom: 1px solid #ddd;
14 padding: 12px;
15}
16
17td a,
18td a:visited {
19 color: black;
20}
21
22td:not(:last-child) {
23 border-left:1px solid #ccc;
24 border-right: 1px solid #ccc;
25}
26
27tr: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";
5
6const NoteList = () => {
7 const {notes} = useFetch('note')
8
9 return (
10 <TableList
11 data={notes}
12 fieldFormatter={{
13 title: (title, dataRow) => [
14 <Link
15 to={`/edit-note/${dataRow.id}`}
16 key='1'
17 >
18 edit
19 </Link>,
20 <span key="2">
21 &nbsp;{
22 title
23 }
24 </span>
25 ],
26 dateCreated: date => new Date(date).toLocaleString()
27 }}
28 />
29 );
30};
31
32export 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';
10
11function 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}
34
35export 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

Share article



neohed © 2022