⚠️ The first part of this tutorial is based on InertiaJs
prior to version 1.0
. Towards the end of this tutorial, we will tackle upgrading InertiaJs
.
This is Part 2 of our Django, InertiaJs & React/Vite tutorial. If you have not looked at Part 1, I would recommend that you check it out here.
ℹ️ To make it easier for you to pick up where we left off or to make sure your project is in working order before we proceed, check out the repo here. Be sure to check the readme before continuing.
In this part of the tutorial series, we are going to be looking at a few things:
- Integrating TypeScript
- Installing Material UI (you can use any component library you want)
- Implementing the contact app
- Backend
- models, views
- Frontend
- Pages and components
- Upgrading InertiaJs to 1.0
Let's get to it, shall we?
⚠️ You may see an error running the project on (MacOS) after cloning the repo that looks like zsh: permission denied: ./manage.py
, you will need to change the permission on that file by using chmod.
# Review the contents of any file that you are going to make executable before making it executable
chmod +x manage.py
The manage.py
file should look like what is below
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'contact_manager.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()
Optional: Installing Prettier (It just makes sense)
Installing Prettier is optional, but still a pretty good idea 😏. So let's do that and set it up before proceeding.
- Install it using the command below
npm i -D prettier
- Create a prettier configuration file
.prettierrc.json
in the root of the project and copy and paste the content below (minimal prettier configuration)
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5"
}
- Depending on your editor or IDE, you may need to tell it to use Prettier.
Integrating TypeScript
Now that we have a working project, let's make it even better with TypeScript. Adding TypeScript is going to make our lives easier in the long run, so let's add it now.
- Create a branch from main, we'll call it
part-2-ts-and-forms
or anything you want
git checkout -b part-2-integrating-ts-and-forms
- Install dependencies (as dev dependences) via npm
# install dev dependencies
npm i -D ts-loader typescript @types/react @types/react-dom
- Initialise TypeScript with some presets
npx tsc --init
- Replace the contents of the generated
tsconfig.json
file with what is shown below
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"types": ["vite/client"],
},
"include": ["react-app/src"],
}
- Don't forget to add the
tsconfig.json
file to version control (if you're using git) - Change the extensions of all our
.jsx
files to.tsx
with the exception ofmain.jsx
- Rename
react-app/src/pages/Home/index.jsx
to end with.tsx
- Modify our home component to add the necessary interface as shown below
interface HomePageProps {
contacts: string[];
}
export default function Home({ contacts }: HomePageProps) {
return (
<div>
<h1>Contact List</h1>
{Array.isArray(contacts) && contacts.length ? (
<ul>
{contacts.map((contact) => (
<li key={contact}>{contact}</li>
))}
</ul>
) : (
<p>No contacts yet...</p>
)}
</div>
);
}
ℹ️ We will replace the contents of this file with something else eventually but, for right now, we don't want TypeScript to complain
- Rename
react-app/src/components/Layout.jsx
to end with.tsx
- Modify the layout component to add the necessary interface as shown below
import React from 'react';
import type { FC } from 'react';
interface LayoutProps {
children: React.ReactNode;
}
const Layout: FC<LayoutProps> = ({ children }) => {
return (
<main>
<div>{children}</div>
</main>
);
};
export default (page: React.ReactNode | React.ReactElement) => (
<Layout>{page}</Layout>
);
- Update our
main.jsx
component as follows
// change this line
const pages = import.meta.glob('./pages/**/*.jsx');
// to
const pages = import.meta.glob('./pages/**/*.tsx');
// change this line
const page = (await pages[`./pages/${name}.jsx`]()).default;
// to
const page = (await pages[`./pages/${name}.tsx`]()).default;
ℹ️ We will be revisiting the converting of main.jsx
to TypeScript in the upgrade to InertiaJs v1.0 section
- Re-run vite via
npm run dev
along with the Django dev server via./manage.py runserver
and navigate to http://127.0.0.1:8000 and you shouldn't see any errors in the browser console
Implementing the Contact Manager
We're now at the point where we can implement our contact manager application. Keep in mind that this is a somewhat-simplified example web application. We will be skipping authentication to keep this part short. (In a future Django/InertiaJs project, we'll build something more complicated complete with auth) We will need have a way to manage contacts and notes for each contact. Let's do some Django things.
-
Models
- Each model we create will have the these common fields
- Created At
- Updated At
-
Model: Contact
- For each contact, we will want to keep track of the following items - First name
- Last name
- Middle Name
- Email Address
- Phone Number
Create our contact app using django admin as follows
django-admin startapp contact
- If you're using git, go ahead and add all files from the
contact
app we just created to git - Let's also add our newly created
contact
app toINSTALLED_APPS
so that Django can 'see' it 👀 insettings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# django InertiaJS apps
'django_vite',
'inertia',
# our apps
'contact' # <- this line was added
]
- Let's define our models in our model file called...well,
models.py
located atcontact/models.py
import datetime
from django.db import models
# Create your models here.
class AppModel(models.Model):
"""
We are using this as an abstract model so that we can define and reuse common fields across models
"""
created_at = models.DateTimeField(default=datetime.datetime.utcnow)
updated_at = models.DateTimeField(blank=True, null=True)
class Meta:
abstract = True
class Contact(AppModel):
first_name = models.CharField(max_length=50)
middle_name = models.CharField(blank=True, max_length=50, null=True)
last_name = models.CharField(max_length=50)
email_address = models.EmailField(max_length=200)
phone_number = models.CharField(max_length=20)
def __str__(self):
return f'({self.id}) {self.first_name} {self.last_name}'
class ContactNote(AppModel):
contact = models.ForeignKey('Contact', on_delete=models.CASCADE, related_name='ContactNoteOwner')
content = models.TextField()
def __str__(self):
return f'({self.id}) Note for {self.contact.first_name} {self.contact.last_name}'
- With our models created, we can run our migration (this will get rid of the unapplied migrations warning)
# *nix or Mac OS
./manage.py makemigrations
./manage.py migrate
# windows
manage.py makemigrations
manage.py migrate
ℹ️ While we have created a ContactNote
model. I'll leave that up to you to make use of or I can do a Part 2.5 to this guide which covers that.
Django Admin
Let's detour a bit and set up our Django admin so that we can easily create some test data for our application
- Create the django super user account by running the command below
./manage.py createsuperuser
- Fill out the information asked in the prompts. Once done, it should say that the superuser was created successfully.
ℹ️ If you want to see all the commands available, you can just run ./manage.py
- Let's make sure things are working correctly by running the django dev server and navigating to http://127.0.0.1:8000/admin. You should see something like image below
- Let's register our models so that they also show up in the Django's admin interface by modifying the file
contact/admin.py
from django.contrib import admin
from contact import models # There are other ways to do the model imports
# Register your models here.
@admin.register(models.Contact)
class ContactAdmin(admin.ModelAdmin):
pass
@admin.register(models.ContactNote)
class ContactNoteAdmin(admin.ModelAdmin):
pass
- Reload the django admin interface and you should now see our models
- Go ahead and create some contact and some notes via the admin interface and then we can get back to the fun stuff.
- Let's make a slight change to add a
nick_name
field to our contact model
class Contact(AppModel):
first_name = models.CharField(max_length=50)
middle_name = models.CharField(blank=True, max_length=50, null=True)
last_name = models.CharField(max_length=50)
nickname = models.CharField(blank=True, max_length=50, null=True) # <- we added this
email_address = models.EmailField(max_length=200, unique=True)
phone_number = models.CharField(max_length=20, unique=True)
def __str__(self):
return f'({self.id}) {self.first_name} {self.last_name}'
- Let's make migrations and then apply them
./manage.py makemigrations
./manage.py migrate
- Let's set up our model forms which we will use later for validation by creating the file
contact/forms.py
from django.forms import ModelForm
from contact.models import Contact, ContactNote
# We are letting Django's model forms do the work for us. We are only scratching the surface of what can be done with Model forms.
class ContactForm(ModelForm):
"""
Model form used for just validating our data since we're sending data via InertiaJS
"""
class Meta:
model = Contact
exclude = ['created_at', 'updated_at']
class ContactNoteForm(ModelForm):
"""
Model form used for just validating our data since we're sending data via InertiaJS
"""
class Meta:
model = ContactNote
exclude = ['created_at', 'updated_at']
ℹ️ By specifying exclude
, we are telling the model form to use all fields but exclude the ones specified. For more information on Django Model Forms, click here.
- Let's work on a way to get our data from our database by creating a file called
dataSource.py
. We are keeping things simple here to keep the focus onInertiaJs
.
import datetime
from contact.models import Contact, ContactNote
from contact.forms import ContactForm, ContactNoteForm
def get_contact_summary():
"""
Retrieves the count of contacts and contact notes
"""
try:
total_contacts = Contact.objects.count()
except Exception as err:
total_contacts = 0
try:
total_notes = ContactNote.objects.count()
except Exception as err:
total_notes = 0
return {
'totalContacts': total_contacts,
'totalNotes': total_notes,
}
# we'll add more to this later
- Let's make some changes to our
index
view incontact_manager.views
so that we can have our contact summary displayed
from inertia import inertia
from contact.dataSource import get_contact_summary
@inertia('Home/Index')
def index(request):
return {
'summary': get_contact_summary()
}
- Next, we'll modify the corresponding component to show our summary and to make sure things are working. I opted to rewrite the component, but it still works the same way
import { FC } from 'react';
interface HomePageProps {
summary: {
totalContacts: number;
totalNotes: number;
};
}
const HomeIndex: FC<HomePageProps> = ({ summary }) => {
return (
<div>
<h1>Contact Summary</h1>
<div>
<div>
<h2>{summary.totalContacts}</h2>
<p>Total Contacts</p>
</div>
<div>
<h2>{summary.totalNotes}</h2>
<p>Total Contact Notes</p>
</div>
</div>
</div>
);
};
export default HomeIndex;
Now would be a pretty good time to get some styling setup. We'll be using MUI React because we're efficient like that.
- Let's install Material UI
npm i -D @mui/material @emotion/react @emotion/styled @fontsource/roboto
- Let's restructure our
Home/Index.tsx
component so it looks somewhat presentable - We will create a card component so we can display information on the home page nicely
components/MetricCard.tsx
import React from 'react';
import type {FC} from 'react';
import { Card, CardActions, CardContent, Typography } from '@mui/material';
interface MetricCardProps {
actions?: React.ReactElement;
label: string;
value: number | string;
}
export const MetricCard: FC<MetricCardProps> = ({ actions, label, value }) => {
return (
<Card variant="outlined">
<CardContent>
<Typography sx={{ fontSize: 80 }} variant="body1">
{value}
</Typography>
<Typography sx={{ fontSize: 20 }} variant="h2">
{label}
</Typography>
</CardContent>
{actions && <CardActions>{actions}</CardActions>}
</Card>
);
};
- Let's update
Home/Index.tsx
import { FC } from 'react';
import { Inertia } from '@inertiajs/inertia';
import { Box, Button, Grid, Typography } from '@mui/material';
import { MetricCard } from '../../components/MetricCard';
interface HomePageProps {
summary: {
totalContacts: number;
totalNotes: number;
};
}
const HomeIndex: FC<HomePageProps> = ({ summary }) => {
return (
<Box sx={{ flexGrow: 1 }}>
<Typography variant="h2">Contact Summary</Typography>
<Typography variant="body1">
Here's a summary of your contact list and notes
</Typography>
<Box sx={{ flexGrow: 1 }}>
<Grid container direction="row" spacing={1}>
<Grid item xs={12} md={6}>
<MetricCard
actions={
<>
<Button variant="outlined">Add New Contact</Button>{' '}
<Button
onClick={() => Inertia.visit('/contact')}
variant="outlined"
>
View all Contacts
</Button>{' '}
</>
}
label="Total Contacts"
value={summary.totalContacts}
/>
</Grid>
<Grid item xs={12} md={6}>
<MetricCard
actions={
<>
<Button variant="outlined">View all notes</Button>{' '}
</>
}
label="Total Notes"
value={summary.totalNotes}
/>
</Grid>
</Grid>
</Box>
</Box>
);
};
export default HomeIndex;
- Let's add some icons via NPM
npm install -D @mui/icons-material
- Let's update our layout file
components/Layout.tsx
import React from 'react';
import type { FC } from 'react';
import { AppBar, Container, Toolbar, Typography } from '@mui/material';
import ContactsIcon from '@mui/icons-material/Contacts';
interface LayoutProps {
// For our purposes, ReactNode will be fine.
children: React.ReactNode;
}
const Layout: FC<LayoutProps> = ({ children }) => {
return (
<main>
<AppBar position="static">
<Container maxWidth="xl">
<Toolbar disableGutters>
<ContactsIcon sx={{ display: { xs: 'none', md: 'flex' }, mr: 1 }} />
<Typography
variant="h6"
noWrap
component="a"
href="/"
sx={{
mr: 2,
display: { xs: 'none', md: 'flex' },
fontFamily: 'monospace',
fontWeight: 700,
letterSpacing: '.15rem',
color: 'inherit',
textDecoration: 'none',
}}
>
Contact Manager - InertiaJs
</Typography>
</Toolbar>
</Container>
</AppBar>
<div>{children}</div>
</main>
);
};
export default (page: React.ReactNode | React.ReactElement) => (
<Layout>{page}</Layout>
);
Now we're getting somewhere. Let's press on 👍
- Let's work on our contact list functionality by editing the
contact/views.py
from django.shortcuts import redirect
from inertia import inertia
from contact import dataSource
@inertia('Contact/Listing')
def contacts(request):
return dataSource.get_contacts()
- Because we're doing things the typescript way, let's create an interface based on
Contact
model inreact-app/src/interfaces/contact.interface.ts
export interface Contact {
id: number;
first_name: string;
middle_name?: string;
last_name: string;
nickname?: string;
email_address: string;
phone_number: string;
}
- Let's create our contact listing
react-app/src/pages/Contact/Listing.tsx
import React from 'react';
import type { FC } from 'react';
import {
Box,
Divider,
IconButton,
List,
ListItem,
ListItemIcon,
ListItemText,
} from '@mui/material';
import {
DeleteForeverOutlined as DeleteIcon,
EditRounded as EditIcon,
InfoOutlined as InfoIcon,
Person2Rounded as PersonIcon,
} from '@mui/icons-material';
import type { Contact } from '../../interfaces/contact.interface';
interface ListingProps {
contacts: Contact[];
}
const Listing: FC<ListingProps> = ({ contacts }) => {
return (
<List sx={{ width: '100%' }}>
{contacts.map((contact, index) => (
<Box key={`item-${index}`}>
{index ? <Divider /> : null}
<ListItem
key={`contact-${contact.id}`}
alignItems="center"
secondaryAction={
<>
<IconButton>
<InfoIcon />
</IconButton>
<IconButton>
<EditIcon />
</IconButton>
<IconButton>
<DeleteIcon />
</IconButton>
</>
}
>
<ListItemIcon>
<PersonIcon />
</ListItemIcon>
<ListItemText
primary={`${contact.first_name} ${contact.last_name}`}
secondary={contact.phone_number}
/>
</ListItem>
</Box>
))}
</List>
);
};
export default Listing;
Not too shabby for our listing. Of course, you are free to go for maximum style points if you'd like
- Since we're looking at our listing, let's take this time to create a contact info modal component
react-app/src/components/ContactInfoDialog.tsx
import type { FC } from 'react';
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Grid,
ListItemText,
} from '@mui/material';
import { Close as CloseIcon, Edit as EditIcon } from '@mui/icons-material';
import type { Contact } from '../interfaces/contact.interface';
interface ContactInfoDialogProps {
contact?: Contact;
onClose: () => void;
onEdit: () => void;
open: boolean;
}
export const ContactInfoDialog: FC<ContactInfoDialogProps> = ({
contact,
onClose,
onEdit,
open,
}) => {
return (
<Dialog fullWidth maxWidth="sm" onClose={onClose} open={open}>
<DialogTitle>Contact Information</DialogTitle>
<DialogContent>
<Grid container spacing={1}>
<Grid item xs={4}>
<ListItemText
primary={contact?.first_name}
secondary="First Name"
/>
</Grid>
<Grid item xs={4}>
<ListItemText
primary={contact?.middle_name || 'N/A'}
secondary="Middle Name"
/>
</Grid>
<Grid item xs={4}>
<ListItemText primary={contact?.last_name} secondary="Last Name" />
</Grid>
<Grid item xs={12}>
<ListItemText
primary={contact?.nickname || 'N/A'}
secondary="Nickname / Alias"
/>
</Grid>
<Grid item xs={6}>
<ListItemText
primary={contact?.email_address}
secondary="Email Address"
/>
</Grid>
<Grid item xs={6}>
<ListItemText
primary={contact?.phone_number}
secondary="Phone Number"
/>
</Grid>
</Grid>
</DialogContent>
<DialogActions>
<Button onClick={onEdit} startIcon={<EditIcon />}>
Edit Contact
</Button>
<Button onClick={onClose} startIcon={<CloseIcon />}>
Close
</Button>
</DialogActions>
</Dialog>
);
};
- We'll need to edit our listing
react-app/src/pages/Contact/Listing.tsx
so that we can use the newly created dialog
// modified react import
import React, { useCallback, useMemo, useState } from 'react';
import type { FC } from 'react';
import { Inertia } from '@inertiajs/inertia';
import {
Box,
Divider,
Fab,
IconButton,
List,
ListItem,
ListItemIcon,
ListItemText,
} from '@mui/material';
import {
AddCircleOutlineRounded as AddIcon,
DeleteForeverOutlined as DeleteIcon,
EditRounded as EditIcon,
InfoOutlined as InfoIcon,
Person2Rounded as PersonIcon,
} from '@mui/icons-material';
import type { Contact } from '../../interfaces/contact.interface';
import { ContactInfoDialog } from '../../components/ContactInfoDialog';
interface ListingProps {
contacts: Contact[];
}
const Listing: FC<ListingProps> = ({ contacts }) => {
const [selectedContactId, setSelectedContactId] = useState<
number | undefined
>();
// this was added
const closeContactInfoModal = useCallback(() => {
setSelectedContactId(undefined);
}, []);
// this was also added - we'll use this later
const editContact = useCallback((contactId: number) => {
Inertia.visit(`/contact/${contactId}`);
}, []);
const selectedContact: Contact | undefined = useMemo(() => {
try {
return contacts.find((contact) => contact.id === selectedContactId);
} catch (e) {
return undefined;
}
}, [contacts, selectedContactId]);
return (
<>
<List sx={{ width: '100%' }}>
{contacts.map((contact, index) => (
<Box key={`item-${index}`}>
{index ? <Divider /> : null}
<ListItem
key={`contact-${contact.id}`}
alignItems="center"
secondaryAction={
<>
<IconButton onClick={() => setSelectedContactId(contact.id)}>
<InfoIcon />
</IconButton>
<IconButton onClick={() => editContact(contact.id)}>
<EditIcon />
</IconButton>
<IconButton>
<DeleteIcon />
</IconButton>
</>
}
>
<ListItemIcon>
<PersonIcon />
</ListItemIcon>
<ListItemText
primary={`${contact.first_name} ${contact.last_name}`}
secondary={contact.phone_number}
/>
</ListItem>
</Box>
))}
</List>
<ContactInfoDialog
contact={selectedContact}
onClose={closeContactInfoModal}
onEdit={
// we'll use this when we're ready to edit contacts
selectedContact ? () => editContact(selectedContact.id) : () => {}
}
open={!!selectedContact}
/>
<Fab
color="primary"
onClick={() => Inertia.visit('/contact/add')}
sx={{ bottom: 20, position: 'absolute', right: 20 }}
>
<AddIcon />
</Fab>
</>
);
};
export default Listing;
Not bad at all for our contact information dialog, right? Let's proceed to the edit functionality and then we'll handle the delete.
- We're going to have a reusable notification component, so let's implement that right now
react-app/src/components/Notification.tsx
import React from 'react';
import type { FC } from 'react';
import type { AlertColor } from '@mui/material';
import { Alert, Snackbar } from '@mui/material';
interface NotificationProps {
closeNotification: () => void;
duration?: number;
notificationOpen: boolean;
notificationText: string;
notificationType?: AlertColor;
}
export const Notification: FC<NotificationProps> = ({
closeNotification,
duration = 3000,
notificationOpen,
notificationType,
notificationText,
}) => {
return (
<Snackbar
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
autoHideDuration={duration}
onClose={closeNotification}
open={notificationOpen}
>
<Alert
onClose={closeNotification}
severity={notificationType}
variant="filled"
>
{notificationText}
</Alert>
</Snackbar>
);
};
- Let's go ahead and implement the edit contact page
react-app/src/pages/Contact/ContactForm.tsx
import React, { useCallback, useEffect, useState } from 'react';
import type { FC, FormEvent } from 'react';
import { Inertia } from '@inertiajs/inertia';
import { useForm } from '@inertiajs/inertia-react';
import {
Box,
Button,
Container,
FormControl,
FormHelperText,
Grid,
Input,
InputLabel,
Typography,
} from '@mui/material';
import type { AlertColor } from '@mui/material';
import { ContactPhone as ContactIcon } from '@mui/icons-material';
import type { Contact } from '../../interfaces/contact.interface';
import { Notification } from '../../components/Notification';
type FormData = {
first_name: string;
middle_name: string;
last_name: string;
nickname: string;
email_address: string;
phone_number: string;
};
const SUBMIT_RESPONSE_MESSAGES: Record<AlertColor, string> = {
success: 'Contact information was saved',
error: 'There was a problem with saving contact information.',
info: '',
warning: '',
};
interface ContactFormProps {
contact?: Contact;
}
const ContactForm: FC<ContactFormProps> = ({ contact }) => {
const {
clearErrors,
data,
errors,
post: createContact,
processing,
setData,
put: updateContact,
} = useForm({
first_name: '',
middle_name: '',
last_name: '',
nickname: '',
email_address: '',
phone_number: '',
});
const [contactId, setContactId] = useState<number | undefined>();
const [notificationType, setNotificationType] = useState<AlertColor>();
const [notificationOpen, setNotificationOpen] = useState(false);
const [notificationText, setNotificationText] = useState('');
useEffect(() => {
if (contact) {
setContactId(contact.id);
setData({
...contact,
middle_name: contact.middle_name || '',
nickname: contact.nickname || '',
} as FormData);
}
}, [contact]);
const closeNotification = useCallback(() => {
if (notificationType === 'success') {
Inertia.visit('/contact');
}
setNotificationOpen(false);
setNotificationText('');
}, [notificationType]);
const handleNotification = useCallback((notificationType: AlertColor) => {
setNotificationType(notificationType);
setNotificationText(SUBMIT_RESPONSE_MESSAGES[notificationType]);
setNotificationOpen(true);
}, []);
const submitContactData = useCallback(
(e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
clearErrors();
contact
? updateContact(`/contact/${contactId}/`, {
onError: () => handleNotification('error'),
onSuccess: () => {
handleNotification('success');
setTimeout(() => {
Inertia.visit('/contact');
}, 3000);
},
})
: createContact('', {
onError: () => handleNotification('error'),
onSuccess: () => {
handleNotification('success');
setTimeout(() => {
Inertia.visit('/contact');
}, 3000);
},
});
},
[contactId, data]
);
return (
<Container maxWidth="md" sx={{ pt: 2, width: '100%' }}>
<Box
sx={{
alignItems: 'center',
display: 'flex',
flexDirection: 'row',
mb: 2,
}}
>
<ContactIcon fontSize="large" />
<Typography sx={{ fontSize: 40, ml: 1 }} variant="h1">
{contact ? 'Update' : 'Add New'} Contact
</Typography>
</Box>
<form onSubmit={submitContactData}>
<Grid container spacing={2}>
<Grid item sm={4} xs={12}>
<FormControl error={!!errors.first_name} fullWidth>
<InputLabel htmlFor="first-name">First Name</InputLabel>
<Input
id="first-name"
onChange={(e) => setData('first_name', e.target.value)}
placeholder="First Name..."
required
value={data.first_name}
/>
{errors.first_name ? (
<FormHelperText sx={{ ml: 0 }}>
{errors.first_name}
</FormHelperText>
) : null}
</FormControl>
</Grid>
<Grid item sm={4} xs={12}>
<FormControl error={!!errors.middle_name} fullWidth>
<InputLabel htmlFor="middle-name">Middle Name</InputLabel>
<Input
id="middle-name"
onChange={(e) => setData('middle_name', e.target.value)}
placeholder="Middle Name..."
type="text"
value={data.middle_name}
/>
{errors.middle_name ? (
<FormHelperText sx={{ ml: 0 }}>
{errors.middle_name}
</FormHelperText>
) : null}
</FormControl>
</Grid>
<Grid item sm={4} xs={12}>
<FormControl error={!!errors.last_name} fullWidth>
<InputLabel htmlFor="last-name">Last Name</InputLabel>
<Input
id="last-name"
onChange={(e) => setData('last_name', e.target.value)}
placeholder="Last Name..."
required
value={data.last_name}
/>
{errors.last_name ? (
<FormHelperText sx={{ ml: 0 }}>
{errors.last_name}
</FormHelperText>
) : null}
</FormControl>
</Grid>
<Grid item xs={12}>
<FormControl error={!!errors.nickname} fullWidth>
<InputLabel>Nickname</InputLabel>
<Input
error={!!errors.nickname}
id="nickname"
onChange={(e) => setData('nickname', e.target.value)}
placeholder="Nickname..."
value={data.nickname}
/>
{errors.nickname ? (
<FormHelperText sx={{ ml: 0 }}>
{errors.nickname}
</FormHelperText>
) : null}
</FormControl>
</Grid>
<Grid item sm={6} xs={12}>
<FormControl error={!!errors.email_address} fullWidth>
<InputLabel htmlFor="email-address">Email Address</InputLabel>
<Input
id="email-address"
onChange={(e) => setData('email_address', e.target.value)}
placeholder="user@example.com"
required
type="email"
value={data.email_address}
/>
{errors.email_address ? (
<FormHelperText sx={{ ml: 0 }}>
{errors.email_address}
</FormHelperText>
) : null}
</FormControl>
</Grid>
<Grid item sm={6} xs={12}>
<FormControl error={!!errors.phone_number} fullWidth>
<InputLabel htmlFor="phone-number">Phone Number</InputLabel>
<Input
id="phone-number"
onChange={(e) => setData('phone_number', e.target.value)}
placeholder="555-0001"
required
value={data.phone_number}
/>
{errors.phone_number ? (
<FormHelperText sx={{ ml: 0 }}>
{errors.phone_number}
</FormHelperText>
) : null}
</FormControl>
</Grid>
</Grid>
<Box sx={{ mt: 2 }}>
<Button
disabled={processing}
fullWidth
type="submit"
variant="contained"
>
{contact ? 'Update' : 'Save New'} Contact
</Button>
</Box>
</form>
<Notification
closeNotification={closeNotification}
notificationOpen={notificationOpen}
notificationText={notificationText}
notificationType={notificationType}
/>
</Container>
);
};
export default ContactForm;
- Next, let's set up our view so that we can navigate to the contact form by editing
contact/views.py
# Add the function to contact/views.py
@inertia('Contact/ContactForm')
def add_contact(request):
if request.method == 'GET':
return {}
result = dataSource.upsert_contact(json.loads(request.body))
return result
- Let's update our contact urls in
contact/urls.py
from django.urls import path
from contact import views
urlpatterns = [
path('', views.contact_list, name='contact-list'),
path('add/', views.add_contact, name='add-contact'),
]
- Let's edit our listing to add a
New Contact
button and hook it up
// add these imports
import { Fab } from '@mui/material';
import { Inertia } from '@inertiajs/inertia';
// Add this near the bottom of the markup in the JSX
<Fab
color="primary"
onClick={() => Inertia.visit('/contact/add')}
sx={{ bottom: 20, position: 'absolute', right: 20 }}
>
<AddIcon />
</Fab>
- Let's try try navigating to our newly created contact form by clicking the FAB we added
- Since we're using Django's Model Forms, we get backend validation that we can tap into. Let's try it out
ℹ️ This would be a good time to also add in some Front-end validation with whatever method you would like.
- Let's try to save a contact and see what happens
- Let's work on implementing the edit functionality by updating our
contact/views.py
file as follows
# add this to end of the file
@inertia('Contact/ContactForm')
def contact(request, pk):
if request.method == 'GET':
data = dataSource.get_contact(pk)
return {
'success': True if data is not None else False,
'contact': data
}
result = dataSource.upsert_contact(json.loads(request.body), pk)
return result
- Update our urls
contact/url.py
file as follows
from django.urls import path
from contact import views
urlpatterns = [
path('', views.contact_list, name='contact-list'),
path('add/', views.add_contact, name='add-contact'),
path('<int:pk>/', views.contact, name='contact'), # <- we added this path
]
With this in place, we should be able to click the edit button from the listing or the contact info modal and have the contact form load with details. The save button should also work since we wired that up earlier. Go ahead and try it.
Let's move on to the delete functionality as no contact manager would be complete without delete functionality.
- Edit
contact/dataSource.py
by adding the function below to the end of the file
def delete_contact(contact_id):
result = {
'errors': '',
'success': False
}
try:
Contact.objects.get(contact_id).delete()
result['success'] = True
except Exception as err:
result['errors'] = err.__str__()
return result
- Let's also update our views
contact/views.py
by adding the following code to the end of the file
def delete_contact(request, pk):
if request.method != 'DELETE':
return redirect('contact_list')
result = dataSource.delete_contact(pk)
if not result['success']:
return result
return redirect('contact-list')
- Let's update our urls
contact/urls.py
so that it looks like what is below
from django.urls import path
from contact import views
urlpatterns = [
path('', views.contact_list, name='contact-list'),
path('add/', views.add_contact, name='add-contact'),
path('<int:pk>/', views.contact, name='contact'),
path('<int:pk>/delete/', views.delete_contact), # <- we added this
]
- You guys might notice that I like using dialogs, so we're going to make a question dialog called
QuestionDialog
😼 in our components folder.react-app/src/components/QuestionDialog.tsx
import React from 'react';
import type { FC } from 'react';
import {
Button,
Dialog,
DialogActions,
DialogContentText,
DialogTitle,
DialogContent,
} from '@mui/material';
import { DeleteForeverOutlined as DeleteIcon } from '@mui/icons-material';
import type { Contact } from '../interfaces/contact.interface';
interface QuestionDialogProps {
cancelAction: () => void;
confirmAction: () => void;
contact?: Contact;
open: boolean;
}
export const QuestionDialog: FC<QuestionDialogProps> = ({
cancelAction,
confirmAction,
contact,
open,
}) => {
return (
<Dialog open={open} onClose={cancelAction}>
<DialogTitle id="question-dialog-title">
{`Delete Contact: ${contact?.first_name} ${contact?.last_name}`}?
</DialogTitle>
<DialogContent>
<DialogContentText id="question-dialog-prompt-text">
{contact
? `You are about to delete ${contact.first_name} ${
contact.last_name
} ${
contact.nickname ? `[${contact.nickname}]` : ''
}. Would you like to continue?`
: ''}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={cancelAction}>No</Button>
<Button
onClick={confirmAction}
startIcon={<DeleteIcon />}
variant="contained"
>
Yes
</Button>
</DialogActions>
</Dialog>
);
};
ℹ️ We could have made a dialog wrapper component since the structure and behavior of the dialogs are very similar (we can do that next time or, you could do it as a challenge)
- Let's update our contact listing as follows
import React, { useCallback, useMemo, useState } from 'react';
import type { FC } from 'react';
import { Inertia } from '@inertiajs/inertia';
import {
Box,
Divider,
Fab,
IconButton,
List,
ListItem,
ListItemIcon,
ListItemText,
} from '@mui/material';
import type { AlertColor } from '@mui/material';
import {
AddCircleOutlineRounded as AddIcon,
DeleteForeverOutlined as DeleteIcon,
EditRounded as EditIcon,
InfoOutlined as InfoIcon,
Person2Rounded as PersonIcon,
} from '@mui/icons-material';
import type { Contact } from '../../interfaces/contact.interface';
import { ContactInfoDialog } from '../../components/ContactInfoDialog';
import { QuestionDialog } from '../../components/QuestionDialog';
import { Notification } from '../../components/Notification';
type DialogType = 'info' | 'question' | 'none';
const CONTACT_LIST_MESSAGES: Record<AlertColor, string> = {
error: 'An error occurred while trying to delete contact.',
info: '',
success: 'Contact was deleted successfully.',
warning: '',
};
interface ListingProps {
contacts: Contact[];
}
const Listing: FC<ListingProps> = ({ contacts }) => {
const [selectedContactId, setSelectedContactId] = useState<
number | undefined
>();
const [dialogType, setDialogType] = useState<DialogType>('none');
const [notificationType, setNotificationType] = useState<AlertColor>();
const [notificationOpen, setNotificationOpen] = useState(false);
const [notificationText, setNotificationText] = useState('');
const closeContactInfoModal = useCallback(() => {
setSelectedContactId(undefined);
setDialogType('none');
}, []);
const editContact = useCallback((contactId: number) => {
Inertia.visit(`/contact/${contactId}`);
}, []);
const showContactInfo = useCallback((contactId: number) => {
setDialogType('info');
setSelectedContactId(contactId);
}, []);
const closeQuestionDialog = useCallback(() => {
setDialogType('none');
setSelectedContactId(undefined);
}, []);
const deleteContact = useCallback((contactId: number) => {
Inertia.delete(`${contactId}/delete`, {
onError: () => {
setNotificationType('error');
setNotificationText(CONTACT_LIST_MESSAGES['error']);
setNotificationOpen(true);
},
onSuccess: () => {
setNotificationType('success');
setNotificationText(CONTACT_LIST_MESSAGES['success']);
setNotificationOpen(true);
},
});
}, []);
const deleteContactPrompt = useCallback((contactId: number) => {
setDialogType('question');
setSelectedContactId(contactId);
}, []);
const resetNotification = useCallback(() => {
setNotificationOpen(false);
setNotificationText('');
setNotificationType(undefined);
}, []);
const selectedContact: Contact | undefined = useMemo(() => {
try {
return contacts.find((contact) => contact.id === selectedContactId);
} catch (e) {
return undefined;
}
}, [contacts, selectedContactId]);
return (
<>
<List sx={{ width: '100%' }}>
{contacts.map((contact, index) => (
<Box key={`item-${index}`}>
{index ? <Divider /> : null}
<ListItem
key={`contact-${contact.id}`}
alignItems="center"
secondaryAction={
<>
<IconButton onClick={() => showContactInfo(contact.id)}>
<InfoIcon />
</IconButton>
<IconButton onClick={() => editContact(contact.id)}>
<EditIcon />
</IconButton>
<IconButton onClick={() => deleteContactPrompt(contact.id)}>
<DeleteIcon />
</IconButton>
</>
}
>
<ListItemIcon>
<PersonIcon />
</ListItemIcon>
<ListItemText
primary={`${contact.first_name} ${contact.last_name}`}
secondary={contact.phone_number}
/>
</ListItem>
</Box>
))}
</List>
<ContactInfoDialog
contact={selectedContact}
onClose={closeContactInfoModal}
onEdit={
selectedContact ? () => editContact(selectedContact.id) : () => {}
}
open={!!selectedContact && dialogType === 'info'}
/>
<QuestionDialog
cancelAction={closeQuestionDialog}
confirmAction={
selectedContact ? () => deleteContact(selectedContact.id) : () => {}
}
contact={selectedContact}
open={!!selectedContact && dialogType === 'question'}
/>
<Fab
color="primary"
onClick={() => Inertia.visit('/contact/add')}
sx={{ bottom: 20, position: 'absolute', right: 20 }}
>
<AddIcon />
</Fab>
<Notification
closeNotification={resetNotification}
notificationOpen={notificationOpen}
notificationText={notificationText}
notificationType={notificationType}
/>
</>
);
};
export default Listing;
At this point, we should be able to delete a record and have it actually disappear from our listing. Go ahead and test it. If it all worked correctly, then you should see something like the image below.
Looking pretty good, right? At this point, everything should be working without issue, if that's the case, feel free celebrate.
- Now would be a good time to commit the code we wrote. You guys already know how to do that since you're pros.
Upgrading to InertiaJS 1.x
We're now at the part where we can go ahead and upgrade our project to InertiaJs 1.x. We will have to do a fair bit of refactoring but I promise that it won't be difficult. You can review the InertiaJs migration docs here.
- Make a new branch for the upgrade
- Go ahead and stop Vite (if already running)
- Remove the old InertiaJs dependencies
# we didn't use @inertiajs/server so it was never installed
npm remove @inertiajs/inertia @inertiajs/inertia-react @inertiajs/progress
- Install the new dependency
npm install -D @inertiajs/react
- In our
react-app/src/main.jsx
file, let's update our InertiaJs import
import React from 'react';
import { createRoot } from 'react-dom/client';
// Remove these lines
// - import { createInertiaApp } from '@inertiajs/inertia-react';
// - import { InertiaProgress } from '@inertiajs/progress';
// Add this line
import { createInertiaApp } from '@inertiajs/react';
import Layout from './components/Layout.tsx';
const pages = import.meta.glob('./pages/**/*.tsx');
document.addEventListener('DOMContentLoaded', () => {
createInertiaApp({
resolve: async (name) => {
const page = (await pages[`./pages/${name}.tsx`]()).default;
page.layout = page.layout || Layout;
return page;
},
setup({ el, App, props }) {
createRoot(el).render(<App {...props} />);
},
}).then();
});
- Let's update the code in
react-app/src/pages/Contact/Listing.tsx
. Here, we're updating our imports and replacing all usages ofInertia.visit()
withrouter.visit()
andInertia.delete()
withrouter.delete()
import React, { useCallback, useMemo, useState } from 'react';
import type { FC } from 'react';
// import { Inertia } from '@inertiajs/inertia';
import { router } from '@inertiajs/react';
import {
Box,
Divider,
Fab,
IconButton,
List,
ListItem,
ListItemIcon,
ListItemText,
} from '@mui/material';
import type { AlertColor } from '@mui/material';
import {
AddCircleOutlineRounded as AddIcon,
DeleteForeverOutlined as DeleteIcon,
EditRounded as EditIcon,
InfoOutlined as InfoIcon,
Person2Rounded as PersonIcon,
} from '@mui/icons-material';
import type { Contact } from '../../interfaces/contact.interface';
import { ContactInfoDialog } from '../../components/ContactInfoDialog';
import { QuestionDialog } from '../../components/QuestionDialog';
import { Notification } from '../../components/Notification';
type DialogType = 'info' | 'question' | 'none';
const CONTACT_LIST_MESSAGES: Record<AlertColor, string> = {
error: 'An error occurred while trying to delete contact.',
info: '',
success: 'Contact was deleted successfully.',
warning: '',
};
interface ListingProps {
contacts: Contact[];
}
const Listing: FC<ListingProps> = ({ contacts }) => {
const [selectedContactId, setSelectedContactId] = useState<
number | undefined
>();
const [dialogType, setDialogType] = useState<DialogType>('none');
const [notificationType, setNotificationType] = useState<AlertColor>();
const [notificationOpen, setNotificationOpen] = useState(false);
const [notificationText, setNotificationText] = useState('');
const closeContactInfoModal = useCallback(() => {
setSelectedContactId(undefined);
setDialogType('none');
}, []);
const editContact = useCallback((contactId: number) => {
router.visit(`/contact/${contactId}`);
}, []);
const showContactInfo = useCallback((contactId: number) => {
setDialogType('info');
setSelectedContactId(contactId);
}, []);
const closeQuestionDialog = useCallback(() => {
setDialogType('none');
setSelectedContactId(undefined);
}, []);
const deleteContact = useCallback((contactId: number) => {
router.delete(`${contactId}/delete`, {
onError: () => {
setNotificationType('error');
setNotificationText(CONTACT_LIST_MESSAGES['error']);
setNotificationOpen(true);
},
onSuccess: () => {
setNotificationType('success');
setNotificationText(CONTACT_LIST_MESSAGES['success']);
setNotificationOpen(true);
},
});
}, []);
const deleteContactPrompt = useCallback((contactId: number) => {
setDialogType('question');
setSelectedContactId(contactId);
}, []);
const resetNotification = useCallback(() => {
setNotificationOpen(false);
setNotificationText('');
setNotificationType(undefined);
}, []);
const selectedContact: Contact | undefined = useMemo(() => {
try {
return contacts.find((contact) => contact.id === selectedContactId);
} catch (e) {
return undefined;
}
}, [contacts, selectedContactId]);
return (
<>
<List sx={{ width: '100%' }}>
{contacts.map((contact, index) => (
<Box key={`item-${index}`}>
{index ? <Divider /> : null}
<ListItem
key={`contact-${contact.id}`}
alignItems="center"
secondaryAction={
<>
<IconButton onClick={() => showContactInfo(contact.id)}>
<InfoIcon />
</IconButton>
<IconButton onClick={() => editContact(contact.id)}>
<EditIcon />
</IconButton>
<IconButton onClick={() => deleteContactPrompt(contact.id)}>
<DeleteIcon />
</IconButton>
</>
}
>
<ListItemIcon>
<PersonIcon />
</ListItemIcon>
<ListItemText
primary={`${contact.first_name} ${contact.last_name}`}
secondary={contact.phone_number}
/>
</ListItem>
</Box>
))}
</List>
<ContactInfoDialog
contact={selectedContact}
onClose={closeContactInfoModal}
onEdit={
selectedContact ? () => editContact(selectedContact.id) : () => {}
}
open={!!selectedContact && dialogType === 'info'}
/>
<QuestionDialog
cancelAction={closeQuestionDialog}
confirmAction={
selectedContact ? () => deleteContact(selectedContact.id) : () => {}
}
contact={selectedContact}
open={!!selectedContact && dialogType === 'question'}
/>
<Fab
color="primary"
onClick={() => router.visit('/contact/add')}
sx={{ bottom: 20, position: 'absolute', right: 20 }}
>
<AddIcon />
</Fab>
<Notification
closeNotification={resetNotification}
notificationOpen={notificationOpen}
notificationText={notificationText}
notificationType={notificationType}
/>
</>
);
};
export default Listing;
- Let's update the code in
react-app/src/pages/Contact/ContactForm.tsx
. Here, we're updating our imports and replacing all usages ofInertia.visit()
withrouter.visit()
. We don't have to change the usage ofuseForm
.
import React, { useCallback, useEffect, useState } from 'react';
import type { FC, FormEvent } from 'react';
// remove these lines
// - import { Inertia } from '@inertiajs/inertia';
// - import { useForm } from '@inertiajs/inertia-react';
// Add this line
import {router, useForm} from '@inertiajs/react';
import {
Box,
Button,
Container,
FormControl,
FormHelperText,
Grid,
Input,
InputLabel,
Typography,
} from '@mui/material';
import type { AlertColor } from '@mui/material';
import { ContactPhone as ContactIcon } from '@mui/icons-material';
import type { Contact } from '../../interfaces/contact.interface';
import { Notification } from '../../components/Notification';
type FormData = {
first_name: string;
middle_name: string;
last_name: string;
nickname: string;
email_address: string;
phone_number: string;
};
const SUBMIT_RESPONSE_MESSAGES: Record<AlertColor, string> = {
success: 'Contact information was saved',
error: 'There was a problem with saving contact information.',
info: '',
warning: '',
};
interface ContactFormProps {
contact?: Contact;
}
const ContactForm: FC<ContactFormProps> = ({ contact }) => {
const {
clearErrors,
data,
errors,
post: createContact,
processing,
setData,
put: updateContact,
} = useForm({
first_name: '',
middle_name: '',
last_name: '',
nickname: '',
email_address: '',
phone_number: '',
});
const [contactId, setContactId] = useState<number | undefined>();
const [notificationType, setNotificationType] = useState<AlertColor>();
const [notificationOpen, setNotificationOpen] = useState(false);
const [notificationText, setNotificationText] = useState('');
useEffect(() => {
if (contact) {
setContactId(contact.id);
setData({
...contact,
middle_name: contact.middle_name || '',
nickname: contact.nickname || '',
} as FormData);
}
}, [contact]);
const closeNotification = useCallback(() => {
if (notificationType === 'success') {
router.visit('/contact');
}
setNotificationOpen(false);
setNotificationText('');
}, [notificationType]);
const handleNotification = useCallback((notificationType: AlertColor) => {
setNotificationType(notificationType);
setNotificationText(SUBMIT_RESPONSE_MESSAGES[notificationType]);
setNotificationOpen(true);
}, []);
const submitContactData = useCallback(
(e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
clearErrors();
contact
? updateContact(`/contact/${contactId}/`, {
onError: () => handleNotification('error'),
onSuccess: () => {
handleNotification('success');
setTimeout(() => {
router.visit('/contact');
}, 3000);
},
})
: createContact('', {
onError: () => handleNotification('error'),
onSuccess: () => {
handleNotification('success');
setTimeout(() => {
router.visit('/contact');
}, 3000);
},
});
},
[contactId, data]
);
return (
<Container maxWidth="md" sx={{ pt: 2, width: '100%' }}>
<Box
sx={{
alignItems: 'center',
display: 'flex',
flexDirection: 'row',
mb: 2,
}}
>
<ContactIcon fontSize="large" />
<Typography sx={{ fontSize: 40, ml: 1 }} variant="h1">
{contact ? 'Update' : 'Add New'} Contact
</Typography>
</Box>
<form onSubmit={submitContactData}>
<Grid container spacing={2}>
<Grid item sm={4} xs={12}>
<FormControl error={!!errors.first_name} fullWidth>
<InputLabel htmlFor="first-name">First Name</InputLabel>
<Input
id="first-name"
onChange={(e) => setData('first_name', e.target.value)}
placeholder="First Name..."
required
value={data.first_name}
/>
{errors.first_name ? (
<FormHelperText sx={{ ml: 0 }}>
{errors.first_name}
</FormHelperText>
) : null}
</FormControl>
</Grid>
<Grid item sm={4} xs={12}>
<FormControl error={!!errors.middle_name} fullWidth>
<InputLabel htmlFor="middle-name">Middle Name</InputLabel>
<Input
id="middle-name"
onChange={(e) => setData('middle_name', e.target.value)}
placeholder="Middle Name..."
type="text"
value={data.middle_name}
/>
{errors.middle_name ? (
<FormHelperText sx={{ ml: 0 }}>
{errors.middle_name}
</FormHelperText>
) : null}
</FormControl>
</Grid>
<Grid item sm={4} xs={12}>
<FormControl error={!!errors.last_name} fullWidth>
<InputLabel htmlFor="last-name">Last Name</InputLabel>
<Input
id="last-name"
onChange={(e) => setData('last_name', e.target.value)}
placeholder="Last Name..."
required
value={data.last_name}
/>
{errors.last_name ? (
<FormHelperText sx={{ ml: 0 }}>
{errors.last_name}
</FormHelperText>
) : null}
</FormControl>
</Grid>
<Grid item xs={12}>
<FormControl error={!!errors.nickname} fullWidth>
<InputLabel>Nickname</InputLabel>
<Input
error={!!errors.nickname}
id="nickname"
onChange={(e) => setData('nickname', e.target.value)}
placeholder="Nickname..."
value={data.nickname}
/>
{errors.nickname ? (
<FormHelperText sx={{ ml: 0 }}>
{errors.nickname}
</FormHelperText>
) : null}
</FormControl>
</Grid>
<Grid item sm={6} xs={12}>
<FormControl error={!!errors.email_address} fullWidth>
<InputLabel htmlFor="email-address">Email Address</InputLabel>
<Input
id="email-address"
onChange={(e) => setData('email_address', e.target.value)}
placeholder="user@example.com"
required
type="email"
value={data.email_address}
/>
{errors.email_address ? (
<FormHelperText sx={{ ml: 0 }}>
{errors.email_address}
</FormHelperText>
) : null}
</FormControl>
</Grid>
<Grid item sm={6} xs={12}>
<FormControl error={!!errors.phone_number} fullWidth>
<InputLabel htmlFor="phone-number">Phone Number</InputLabel>
<Input
id="phone-number"
onChange={(e) => setData('phone_number', e.target.value)}
placeholder="555-0001"
required
value={data.phone_number}
/>
{errors.phone_number ? (
<FormHelperText sx={{ ml: 0 }}>
{errors.phone_number}
</FormHelperText>
) : null}
</FormControl>
</Grid>
</Grid>
<Box sx={{ mt: 2 }}>
<Button
disabled={processing}
fullWidth
type="submit"
variant="contained"
>
{contact ? 'Update' : 'Save New'} Contact
</Button>
</Box>
</form>
<Notification
closeNotification={closeNotification}
notificationOpen={notificationOpen}
notificationText={notificationText}
notificationType={notificationType}
/>
</Container>
);
};
export default ContactForm;
- Let's restart Vite and if everything worked as it should, we shouldn't notice any changes or errors.
Hope you found this guide / tutorial useful. And of course, let me know if you have any questions or comments. There might be a Part 2.5 to this tutorial that would cover the Contact Notes section.
References
The following resources were used in the creation of this part of the tutorial
- InertiaJs migration guide https://inertiajs.com/upgrade-guide
- Material UI for React https://mui.com/material-ui/getting-started/overview/
- Django Model Form documentation https://docs.djangoproject.com/en/4.1/topics/forms/modelforms/
Top comments (0)