Skip to content

Instantly share code, notes, and snippets.

@naporin0624
Last active July 12, 2022 05:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save naporin0624/3c8c50c8387ed7bd6e8d55ab4fd973e5 to your computer and use it in GitHub Desktop.
Save naporin0624/3c8c50c8387ed7bd6e8d55ab4fd973e5 to your computer and use it in GitHub Desktop.
useDialog
type Success<T> = { success: true, value: T }
type Fail<E> = { success: false, error?: E }
export type Result<T, E> = Success<T> | Fail<E>
export const success = <T>(value: T): Result<T, never> => ({
success: true,
value,
})
export const fail = <E>(error?: E): Result<never, E> => ({
success: false,
error,
})
export const isSuccess = <T, E>(result: Result<T, E>): result is Success<T> => result.success
export const isFail = <T, E>(result: Result<T, E>): result is Fail<E> => !result.success
export const unwrap = <T, E>(result: Result<T, E>): T => {
if (result.success) return result.value
throw result.error
}

useDialog

hooks to control the opening and closing of the dialog.

Usage

Simple Usage

import React from "react";
import { useDialog } from "~/hooks"
import { Dialog, DialogContent, DialogActions } from "@mui/material"

const Component = () => {
  const dialog = useDialog<{ content: string }>()
  const executeSomething = async () => {
    /** pending until onOk or onCancel is executed  */
    const result = await dialog.showDialog()

    if (result.success) {
      /** fired onOk  */
    } else {
      /** fired onCancel */
    }
  }

  return (
    <>
      <button onClick={executeSomething}>open</button>

      <Dialog open={dialog.open}>
        <DialogContent>
          {/** something */}
        </DialogContent>

        <DialogActions>
          <Button onClick={dialog.onCancel}>cancel</Button>
          <Button onClick={dialog.onOk}>ok</Button>
        </DialogActions>
      </Dialog>
    </>
  )
}

Pass props from the open function to dialog.

import React from "react";
import { useDialog } from "~/hooks"
import { Dialog, DialogContent, DialogActions } from "@mui/material"

const Component = () => {
  const dialog = useDialog<{ content: string }>()
  const executeSomething = async () => {
    const result = await dialog.showDialog({ content: "hello dialog" })

    if (result.success) {
      // 
    } else {
      // 
    }
  }

  return (
    <>
      <button onClick={executeSomething}>open</button>

      <Dialog open={dialog.open}>
        <DialogContent>
          {dialog.open && (
            <p>{dialog.props.content}</p>
          )}
        </DialogContent>

        <DialogActions>
          <Button onClick={dialog.onCancel}>cancel</Button>
          <Button onClick={dialog.onOk}>ok</Button>
        </DialogActions>
      </Dialog>
    </>
  )
}

Receive data from the dialog.

import React from "react";
import { useDialog } from "~/hooks"
import { Dialog, DialogContent, DialogActions } from "@mui/material"

const Component = () => {
  const dialog = useDialog<{ content: string }, { message: string }>()
  const executeSomething = async () => {
    const result = await dialog.showDialog({ content: "hello dialog" })

    if (result.success) {
      // value is dialog output
      const value = result.value
    } else {
      // 
    }
  }

  return (
    <>
      <button onClick={executeSomething}>open</button>

      <UserInputDialog
        open={dialog.open}
        onCancel={dialog.onCancel}
        onSubmit={dialog.onOk}
      />
    </>
  )
}

type UserInputDialogProps = {
  open?: boolean
  onCancel?: () => void
  onSubmit?: (output: { message: string }) => void
}
const UserInputDialog = (props) => {
  /** something */
}
``
import { act, cleanup, renderHook } from '@testing-library/react-hooks'
import { fail, success } from '~/utils/result'
import { useDialog } from "."
describe('useDialog', () => {
afterEach(async () => {
await cleanup()
})
it('set initialData', () => {
const props = { hoge: 'huga' }
const { result } = renderHook(() => useDialog({ open: true, props }))
if (result.current.open) {
expect(result.current.props).toEqual(props)
} else {
expect.assertions(1)
}
})
it('open dialog', () => {
const { result } = renderHook(() => useDialog())
expect(result.current.open).toEqual(false)
act(() => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
result.current.showDialog()
})
expect(result.current.open).toEqual(true)
})
it('toggle open state', () => {
const { result } = renderHook(() => useDialog())
expect(result.current.open).toEqual(false)
act(() => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
result.current.showDialog()
})
expect(result.current.open).toEqual(true)
act(() => {
result.current.onOk()
})
expect(result.current.open).toEqual(false)
})
it('receive data from dialog', async () => {
const { result } = renderHook(() => useDialog<undefined, number>())
const promise = result.current.showDialog()
act(() => {
result.current.onOk(1)
})
await expect(promise).resolves.toEqual(success(1))
})
it('receive error from dialog', async () => {
const { result } = renderHook(() => useDialog<undefined, number>())
const promise = result.current.showDialog()
act(() => {
result.current.onCancel()
})
await expect(promise).resolves.toEqual(fail(undefined))
})
})
import { useCallback, useMemo, useState } from 'react'
import { Result, success, fail } from '~/utils/result'
type DialogState<T> = {
open: true;
props: T
} | {
open: false
};
type Awaiter<E, T> = {
resolve: (value: T) => void;
reject: (error: E) => void;
};
type State<Input, Output> = DialogState<Input> & Partial<Awaiter<Error, Result<Output, undefined>>>;
type RequireOrOptional<T> = T extends undefined ? [] : [value: T]
type Dialog<Input, Output> = DialogState<Input> & {
showDialog: (...input: RequireOrOptional<Input>) => Promise<Result<Output, unknown>>
onOk: (...input: RequireOrOptional<Output>) => void
onCancel: () => void
}
export const useDialog = <ModalInput = undefined, ModalOutput = undefined>
(initialState?: DialogState<ModalInput>):Dialog<ModalInput, ModalOutput> => {
const [dialogState, setDialogState] = useState<State<ModalInput, ModalOutput>>(
initialState ?? { open: false },
)
const showDialog = useCallback(
(...args: RequireOrOptional<ModalInput>) => {
const [props] = args
return new Promise<Result<ModalOutput, undefined>>(
(resolve, reject) => {
setDialogState((previous) => ({
...previous,
open: true,
props: props as ModalInput,
resolve,
reject,
}))
},
)
},
[],
)
const hideDialog = useCallback(() => {
setDialogState({ open: false })
}, [])
const onOk = useCallback(
(...args: RequireOrOptional<ModalOutput>) => {
const [value] = args
dialogState.resolve?.(success(value as ModalOutput))
hideDialog()
},
[hideDialog, dialogState],
)
const onCancel = useCallback(() => {
dialogState.resolve?.(fail())
hideDialog()
}, [hideDialog, dialogState])
return useMemo(
() => ({
showDialog,
onOk,
onCancel,
...dialogState.open ? { open: true, props: dialogState.props } : { open: false },
}),
[showDialog, onOk, onCancel, dialogState],
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment