End to End React with Prisma 2
Start at the beginning and work your way through this project. The code for each step as well as the finished project can be found in the Github repository.
22. Start the NewEditItem component
Objective: Start a component for making new items.
In this step we will be creating a component that we will be able to use for creating new or editing existing items. For now we will just focus on the new
capability, but after we will turn to setting up the edit
functionality.
We will set up a state that will store an empty feed or bundle in it which we then will update as people write text in the fields. We will store this state as currentItem
and set it using setState
. Our visual component will be a form that has a number of things inside- we are blocking out
components/newEditItem.tsx
tsx
import { useMutation } from '@apollo/client'import { useState } from 'react'import {CREATE_BUNDLE_MUTATION,CREATE_FEED_MUTATION,} from '../utils/api/graphql/mutations'import {ActionType,BadgeFieldName,BundleState,FeedState,ItemType,NewItemState,} from '../utils/types'import { BadgeList } from './badgeList'import { ErrorSign, WaitingClock } from './svg'export const NewEditItem = ({ type }: { type: ItemType }) => {const isFeed = type === ItemType.FeedTypeconst initialFeed: FeedState = { name: '', url: '', tags: [] }const initialBundle: BundleState = {name: '',description: '',tags: [],feeds: [],}const initialState: NewItemState = isFeed ? initialFeed : initialBundleconst inputFields = isFeed ? ['name', 'url'] : ['name', 'description']const [currentItem, setItem] = useState<NewItemState>(initialState)const [createItemMutation,{ loading: createLoading, error: createError },] = useMutation(isFeed ? CREATE_FEED_MUTATION : CREATE_BUNDLE_MUTATION)if (createLoading) {return <WaitingClock className="my-20 h-10 w-10 text-gray-500 m-auto" />}if (createError) {return <ErrorSign className="my-20 h-10 w-10 text-gray-500 m-auto" />}return (<><formonSubmit={e => {e.preventDefault()}}><div className="grid grid-cols-12 gap-4 rounded-md border-4 my-4 py-2 px-4"><h3 className="col-span-12 text-lg font-medium py-2">{isFeed ? `New Feed` : `New Bundle`}</h3><div className="col-span-6">{inputFields.map(name => (<p key={name}>{name}</p>))}<div className={`py-4 ${isFeed ? null : `pt-28`}`}><inputclassName={`py-4 ${`bg-${isFeed ? 'green' : 'purple'}-400`} hover:bg-${isFeed ? 'green' : 'purple'}-700 text-white font-bold px-12 rounded`}type="submit"/></div></div><div className="col-span-6"><div className="py-2"><label className="block py-2">Tags:</label><div className="grid grid-cols-3 gap-2"><BadgeListfieldName={BadgeFieldName.tags}action={ActionType.CREATE}setItem={setItem}item={currentItem}/></div></div><div className="py-2"><label className="block py-2">Add New Tag:</label></div>{isFeed ? null : (<><div className="py-2"><label className="block py-2">Feeds:</label><div className="grid grid-cols-3 gap-2"><BadgeListfieldName={BadgeFieldName.feeds}action={ActionType.CREATE}setItem={setItem}item={currentItem}/></div></div><div className="py-2"><label className="block py-2">Add New Feed:</label></div></>)}</div></div></form></>)}
We have to make sure to add the NewItemState
to our types file- it is just either a FeedState
or BundleState
.
utils/types.ts
ts
// other types aboveexport type NewItemState = FeedState | BundleState
Let's modify our bundles and feeds page. These changes will be similar between the two- we use the useFetchUser
hook to get the user object and if the user is logged in, we will show a div that has a + sign and either feed or bundle depending on which page it is. When you click on it, it will switch to a - sign and we will display the <NewEditItem>
component that we just created.
pages/bundles.tsx
tsx
import { useState } from 'react'import { NewEditItem } from '../components/newEditItem'import { Layout } from '../components/layout'import { ItemType, SelectedFeedState } from '../utils/types'import { ItemList } from '../components/itemList'import { useFetchUser } from '../utils/user'import { Minus, Plus } from '../components/svg'const BundlesPage = () => {const { user, loading } = useFetchUser()const initialSelected: SelectedFeedState = {id: null,feeds: [],editMode: false,newMode: false,}const [selected, setSelected] = useState(initialSelected)return (<Layout><div className="grid grid-cols-2"><h3 className="grid-cols-1 justify-start flex text-lg font-medium py-4">Bundles Page</h3>{user ? (<divonClick={e => {e.persist()setSelected(currState => ({...currState,newMode: !currState.newMode,editMode: false,}))}}className="flex grid-cols-1 justify-end cursor-pointer">{selected.newMode ? (<MinusclassName={`h-6 w-6 text-${selected.newMode ? `gray` : `blue`}-500 mt-4`}/>) : (<PlusclassName={`h-6 w-6 text-${selected.newMode ? `gray` : `blue`}-500 mt-4`}/>)}<h3className={`grid-cols-1 justify-start flex text-lg font-medium py-4 text-${selected.newMode ? `gray` : `blue`}-500`}>New Bundle</h3></div>) : null}</div>{(selected.editMode || selected.newMode) && user ? (<NewEditItem type={ItemType.BundleType} />) : null}<ItemListtype={ItemType.BundleType}useSelected={true}allowEdits={true}selected={selected}setSelected={setSelected}/></Layout>)}export default BundlesPage
pages/feeds.tsx
tsx
import { ItemList } from '../components/itemList'import { NewEditItem } from '../components/newEditItem'import { Layout } from '../components/layout'import { ItemType, SelectedFeedState } from '../utils/types'import { useFetchUser } from '../utils/user'import { useState } from 'react'import { Minus, Plus } from '../components/svg'const FeedsPage = () => {const { user, loading } = useFetchUser()const initialSelected: SelectedFeedState = {id: null,feeds: [],editMode: false,newMode: false,}const [selected, setSelected] = useState(initialSelected)return (<Layout><div className="grid grid-cols-2"><h3 className="grid-cols-1 justify-start flex text-lg font-medium py-4">Feeds Page</h3>{user ? (<divonClick={e => {e.persist()setSelected(currState => ({...currState,newMode: !currState.newMode,editMode: false,}))}}className="flex grid-cols-1 justify-end">{selected.newMode ? (<MinusclassName={`h-6 w-6 text-${selected.newMode ? `gray` : `blue`}-500 mt-4`}/>) : (<PlusclassName={`h-6 w-6 text-${selected.newMode ? `gray` : `blue`}-500 mt-4`}/>)}<h3className={`grid-cols-1 justify-start flex text-lg font-medium py-4 text-${selected.newMode ? `gray` : `blue`}-500`}>New Feed</h3></div>) : null}</div>{(selected.editMode || selected.newMode) && user ? (<NewEditItem type={ItemType.FeedType} />) : null}<ItemListtype={ItemType.FeedType}useSelected={true}allowEdits={true}selected={selected}setSelected={setSelected}/></Layout>)}export default FeedsPage
We also need to add + or - svg in our svg.tsx
file:
components/svg.tsx
tsx
// other svg components aboveexport const Plus = ({ className }) => (<svgxmlns="http://www.w3.org/2000/svg"fill="none"viewBox="0 0 24 24"stroke="currentColor"className={className}><pathstrokeLinecap="round"strokeLinejoin="round"strokeWidth={2}d="M12 6v6m0 0v6m0-6h6m-6 0H6"/></svg>)export const Minus = ({ className }) => (<svgxmlns="http://www.w3.org/2000/svg"fill="none"viewBox="0 0 24 24"stroke="currentColor"className={className}><pathstrokeLinecap="round"strokeLinejoin="round"strokeWidth={2}d="M20 12H4"/></svg>)
Now test it out and make sure it works- when you click it should pop open the new item box with a submit button and a bunch of stubbed out areas that we will add fields to. The first one is the <GenerateInputField>
component.
components/generateInputField.tsx
tsx
import { Dispatch, SetStateAction } from 'react'import { BundleState, FeedState } from '../utils/types'export const GenerateInputField = ({currentItem,name,changeHandler,}: {name: stringcurrentItem: FeedState | BundleStatechangeHandler: Dispatch<SetStateAction<FeedState | BundleState>>}) => (<div className="py-2"><label className="block py-2">{name.charAt(0).toUpperCase() + name.slice(1)}:</label><inputclassName="border-4 rounded w-full py-2 px-3"value={currentItem[name]}onChange={e => {e.persist()changeHandler(curr => ({ ...curr, [name]: e.target.value }))}}/></div>)
This component takes a name component which will be the title of the property that it is in charge of, the currentItem
state that controls the entire form and the changeHandler
which is in charge of updating the field. We display a div that has a label and input in it. When the input changes we will call the change handler and selectively update that particular field while leaving everything else alone.
Now we can update the inputFields.map
to include this <GenerateInputField>
component that we just created:
components/newEditItem.tsx
tsx
import { GenerateInputField } from './generateInputField'// more of the component here{inputFields.map(name => (<GenerateInputFieldkey={`${type}-${name}`}currentItem={currentItem}name={name}changeHandler={setItem}/>))}// the rest of the component here
Now when we open up the new item section we will see fields for the name and either description or url. Try it out! It should actually work- as we type the new contents of the field will get saved to the local state on the page.
The missing piece before we add our create mutation to the mix is to get our tags and feeds under control, and we will do that next.