Archived student lesson

Start the NewEditItem component

Part of End to End React with Prisma 2

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

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.FeedType
  const initialFeed: FeedState = { name: '', url: '', tags: [] }
  const initialBundle: BundleState = {
    name: '',
    description: '',
    tags: [],
    feeds: [],
  }
  const initialState: NewItemState = isFeed ? initialFeed : initialBundle
  const 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 (
    <>
      <form
        onSubmit={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`}`}>
              <input
                className={`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">
                <BadgeList
                  fieldName={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">
                    <BadgeList
                      fieldName={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

// other types above

export 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

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 ? (
          <div
            onClick={e => {
              e.persist()
              setSelected(currState => ({
                ...currState,
                newMode: !currState.newMode,
                editMode: false,
              }))
            }}
            className="flex grid-cols-1 justify-end cursor-pointer"
          >
            {selected.newMode ? (
              <Minus
                className={`h-6 w-6 text-${
                  selected.newMode ? `gray` : `blue`
                }-500 mt-4`}
              />
            ) : (
              <Plus
                className={`h-6 w-6 text-${
                  selected.newMode ? `gray` : `blue`
                }-500 mt-4`}
              />
            )}
            <h3
              className={`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}
      <ItemList
        type={ItemType.BundleType}
        useSelected={true}
        allowEdits={true}
        selected={selected}
        setSelected={setSelected}
      />
    </Layout>
  )
}
export default BundlesPage

pages/feeds.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 ? (
          <div
            onClick={e => {
              e.persist()
              setSelected(currState => ({
                ...currState,
                newMode: !currState.newMode,
                editMode: false,
              }))
            }}
            className="flex grid-cols-1 justify-end"
          >
            {selected.newMode ? (
              <Minus
                className={`h-6 w-6 text-${
                  selected.newMode ? `gray` : `blue`
                }-500 mt-4`}
              />
            ) : (
              <Plus
                className={`h-6 w-6 text-${
                  selected.newMode ? `gray` : `blue`
                }-500 mt-4`}
              />
            )}
            <h3
              className={`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}
      <ItemList
        type={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

// other svg components above

export const Plus = ({ className }) => (
  <svg
    xmlns="http://www.w3.org/2000/svg"
    fill="none"
    viewBox="0 0 24 24"
    stroke="currentColor"
    className={className}
  >
    <path
      strokeLinecap="round"
      strokeLinejoin="round"
      strokeWidth={2}
      d="M12 6v6m0 0v6m0-6h6m-6 0H6"
    />
  </svg>
)

export const Minus = ({ className }) => (
  <svg
    xmlns="http://www.w3.org/2000/svg"
    fill="none"
    viewBox="0 0 24 24"
    stroke="currentColor"
    className={className}
  >
    <path
      strokeLinecap="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

import { Dispatch, SetStateAction } from 'react'
import { BundleState, FeedState } from '../utils/types'

export const GenerateInputField = ({
  currentItem,
  name,
  changeHandler,
}: {
  name: string
  currentItem: FeedState | BundleState
  changeHandler: Dispatch<SetStateAction<FeedState | BundleState>>
}) => (
  <div className="py-2">
    <label className="block py-2">
      {name.charAt(0).toUpperCase() + name.slice(1)}:
    </label>
    <input
      className="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

import { GenerateInputField } from './generateInputField'

// more of the component  here

{
  inputFields.map(name => (
    <GenerateInputField
      key={`${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.