Motivation
JavaScript browser API has prompt()
function which is a synchronized function for getting text input from user. We sometimes uses that kind of input UI components. However, the natively implemented UI component cannot be customized. I wanted to make it with customized UI and make it awaitable like const value = await prompt();
.
Implementation
Like public react component libraries, I implemented use hook function. I'm exposing only the usePrompt()
because I do not want developers to care about the UI implementation and want them to focus on using it as a capsulized feature.
TyepScript implementation.
import styles from "./style.module.scss"
import { useState, useRef, useCallback } from "react"
import { createPortal } from "react-dom"
type Props = {
open: boolean
value: string
onChange: (value: string) => void
onClose: (value: string | null) => void
}
export function Prompt({
open,
value,
onChange: onValueChange,
onClose
}: Props) {
const onChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
onValueChange(e.target.value)
}, [onValueChange])
const onOkClick = useCallback(() => {
onClose(value)
}, [value, onClose])
const onCancelClick = useCallback(() => {
onClose(null)
}, [onClose])
return createPortal((
open && (
<div className={styles.cover}>
<div className={styles.frame}>
<div>
<input type="text" value={value} onChange={onChange} />
</div>
<div>
<button onClick={onCancelClick}>CANCEL</button>
<button onClick={onOkClick}>OK</button>
</div>
</div>
</div>
)
), document.body)
}
export function usePrompt() {
const [open, setOpen] = useState<boolean>(false)
const [value, setValue] = useState<string>("")
const onCloseRef = useRef<(value: string | null) => void>()
const onClose = useCallback((value: string | null) => {
setOpen(false)
if (onCloseRef.current) {
onCloseRef.current(value)
}
}, [setOpen, onCloseRef])
const onChange = (value: string) => {
setValue(value)
}
return {
open: async (value: string) => {
setOpen(true)
setValue(value)
return new Promise<string|null>((resolve) => {
onCloseRef.current = (value: string | null) => {
resolve(value)
}
})
},
elem: (
<Prompt open={open} value={value} onClose={onClose} onChange={onChange}/>
)
}
}
Style in SASS
.cover {
align-items: center;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
height: 100dvh;
left: 0;
position: fixed;
top: 0;
width: 100dvw;
}
.frame {
background-color: white;
padding: 16px;
}
How to use
import { usePrompt } from "./Prompt"
import { useState } from "react"
function App() {
const { open, elem } = usePrompt()
const [value, setValue] = useState<string>("")
const onOpenClick = () => {
open("Initial value").then((value) => {
setValue(value || "cancelled")
})
}
return (
<>
<div>
<button onClick={onOpenClick}>Open prompt</button>
</div>
{value && <div>Input value: {value}</div>}
{elem}
</>
)
}
You can check my git repo if you want. Hope this helps!
Top comments (0)