Calendar Datepicker Parent/Child Dialog Pattern

CALENDAR DIALOG

Here is our current pattern for rendering a calendar datepicker from a list cell. Any recommendations, tips or improvements or other thoughts are welcome.

I wanted to share for feedback. The toast does not work on successful submission. Setting the function interface as (values: any) => any in the children does not seem like the best practice. And probably ten other things could be improved. It was my first time using hooks like this to manage state updates between parent and child elements.

Thanks for taking the time to read. I hope it helps others.

I am really enjoying working with Redwood. Thanks to the team for being so open and helpful. Also, thanks to @dthyresson for help with the aging calculations using services and sdl.

LIST CELL / PARENT ITEM

The List Cell renders two main components: the list item component and then a dialog popup component.

Two hooks in the List Cell help manage and track state between the components.

export const Success = ({
  bushhogItems,
}: CellSuccessProps<BushhogItemsQuery>) => {
  const [isOpen, setIsOpen] = useState<boolean>(false)
  const [bhItemNumber, setItemNumber] = useState<number>(0)
  const [bhItemName, setItemName] = useState<string>('')

  const updateIsOpen = () => {
    setIsOpen(!isOpen)
  }

  return (
    <div className="bg-white overflow-hidden rounded-md">
      <ul className="divide-y divide-gray-200">
        <li>
          {bushhogItems.map((bushhogItem) => (
            <BushhogItem
              key={bushhogItem.id}
              bushhogItem={bushhogItem}
              updateIsOpen={updateIsOpen}
              setItemNumber={setItemNumber}
              setItemName={setItemName}
            />
          ))}
        </li>
      </ul>
      <CalendarDialog
        isOpen={isOpen}
        bhItemNumber={bhItemNumber}
        bhItemName={bhItemName}
        updateIsOpen={updateIsOpen}
      />
    </div>
  )
}

LIST ITEM COMPONENT / CHILD 1

The list item component is a basic display component. It accepts a visibility toggle function and two hook setter functions from the parent.

Button onClick uses the hook setters to update two variables and also fires the visibility function passed from the parent. This toggles the dialog to open.

interface Props {
  bushhogItem: {
    id: number
    name: string
    lastCutDate: string
    age: number
    color: string
  }
  setItemNumber: (values: any) => any
  setItemName: (values: any) => any
  updateIsOpen: () => void
}

const BushhogItem = ({
  bushhogItem,
  setItemNumber,
  setItemName,
  updateIsOpen,
}: Props) => {
  function openDialog() {
    setItemNumber(bushhogItem.id)
    setItemName(bushhogItem.name)
    updateIsOpen()
  }

  return (
    <div className="px-4 py-4 sm:px-6">
      <div className="flex items-center flex-wrap justify-between">
        <... code shortened ...> 
          <button
            type="button"
            onClick={openDialog}
            className="inline-flex items-center shadow-sm px-2.5 py-0.5 border border-gray-300 text-sm leading-5 font-medium rounded-full text-gray-700 bg-white hover:bg-gray-50"
          >
            Update
          </button>
        </div>
      </div>
      <div className="mt-2 sm:flex sm:justify-between">
       <... code shortened ...> 
      </div>
    </div>
  )
}

DATEPICKER DIALOG / CHILD 2

The datepicker dialog component uses react-day-picker out of the box and tailwind headlessui for the modal dialog. It also uses date-fns to format the dates.

The visibility toggle function, as well as two hook variables, are passed to this component. It uses the variables in the graphql mutation to update the record.

Code shortened to the calendar selection and submit/cancel buttons.

interface Props {
  isOpen: boolean
  bhItemNumber: number
  bhItemName: string
  updateIsOpen: () => void
}

const CalendarDialog = ({
  isOpen,
  bhItemNumber,
  bhItemName,
  updateIsOpen,
}: Props) => {
  const [selected, setSelected] = useState<Date>(new Date())
  const cancelButtonRef = useRef(null)
  const [updateItem] = useMutation(UPDATE, {
    onCompleted: () => {
      toast.success('Update Successful')
    },
  })

  function closeModal() {
    isOpen = false
    updateIsOpen()
  }

  const handleDaySelect = (date: Date) => {
    setSelected(date)
  }

  function submitUpdate(id: number) {
    const input = {
      lastCutDate: format(selected, 'yyyy-MM-dd') + 'T23:59:59Z',
    }
    updateItem({ variables: { id, input } })
    closeModal()
  }

  return (
    <>
      <Transition appear show={isOpen} as={Fragment}>
        <Dialog as="div" className="relative z-10" onClose={closeModal}>
         <Transition.Child>

          <... code shortened ...> 

                  <div className="mt-2">
                    <DayPicker
                      onSelect={handleDaySelect}
                      selected={new Date()}
                      mode="single"
                    />
                  </div>

                  <div className="mt-4">
                    <div className="mt-5 sm:mt-6 sm:grid sm:grid-cols-2 sm:gap-3 sm:grid-flow-row-dense">
                      <button
                        type="button"
                        className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-indigo-600 text-base font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:col-start-2 sm:text-sm"
                        onClick={() => submitUpdate(bhItemNumber)}
                      >
                        Confirm
                      </button>
                      <button
                        type="button"
                        className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:col-start-1 sm:text-sm"
                        onClick={closeModal}
                        ref={cancelButtonRef}
                      >
                        Cancel
                      </button>
                    </div> 
         
         <... code shortened ...> 
 
       </Dialog>
      </Transition>
    </>
  )
}