{children
? children({ index: i, readOnly: i < readOnlyLen, placeholder: i >= min ? 'optional' : undefined })
: = min ? 'optional' : undefined} />}
{options.length - 1 === i && options.length !== max
? fieldArrayHelpers.push(emptyItem)} />
// filler div for col alignment across rows
: }
{options.length - 1 === i &&
<>
{hint && {hint}}
{form.touched[name] && typeof form.errors[name] === 'string' &&
{form.errors[name]}
}
>}
))}
>
)
}}
)
}
export function Checkbox ({
children, label, groupClassName, type = 'checkbox',
hiddenLabel, extra, handleChange, inline, disabled, ...props
}) {
// React treats radios and checkbox inputs differently other input types, select, and textarea.
// Formik does this too! When you specify `type` to useField(), it will
// return the correct bag of props for you
const [field, meta, helpers] = useField({ ...props, type })
return (
{hiddenLabel && {label}}
{
field.onChange(e)
handleChange && handleChange(e.target.checked, helpers.setValue)
}}
/>
{label}
{extra &&
{extra}
}
)
}
export function CheckboxGroup ({ label, groupClassName, children, ...props }) {
const [, meta] = useField(props)
return (
{children}
{/* force the feedback to display with d-block */}
{meta.touched && meta.error}
)
}
const StorageKeyPrefixContext = createContext()
export function Form ({
initial, schema, onSubmit, children, initialError, validateImmediately,
storageKeyPrefix, validateOnChange = true, invoiceable, innerRef, ...props
}) {
const toaster = useToast()
const initialErrorToasted = useRef(false)
const feeButton = useFeeButton()
useEffect(() => {
if (initialError && !initialErrorToasted.current) {
toaster.danger('form error: ' + initialError.message || initialError.toString?.())
initialErrorToasted.current = true
}
}, [])
const clearLocalStorage = useCallback((values) => {
Object.keys(values).forEach(v => {
window.localStorage.removeItem(storageKeyPrefix + '-' + v)
if (Array.isArray(values[v])) {
values[v].forEach(
(iv, i) => {
Object.keys(iv).forEach(k => {
window.localStorage.removeItem(`${storageKeyPrefix}-${v}[${i}].${k}`)
})
window.localStorage.removeItem(`${storageKeyPrefix}-${v}[${i}]`)
})
}
})
}, [storageKeyPrefix])
// if `invoiceable` is set,
// support for payment per invoice if they are lurking or don't have enough balance
// is added to submit handlers.
// submit handlers need to accept { satsReceived, hash, hmac } in their first argument
// and use them as variables in their GraphQL mutation
if (invoiceable && onSubmit) {
const options = typeof invoiceable === 'object' ? invoiceable : undefined
onSubmit = useInvoiceable(onSubmit, options)
}
const onSubmitInner = useCallback(async (values, ...args) => {
try {
if (onSubmit) {
// extract cost from formik fields
// (cost may also be set in a formik field named 'amount')
const cost = feeButton?.total || values?.amount
if (cost) {
values.cost = cost
}
await onSubmit(values, ...args)
if (!storageKeyPrefix) return
clearLocalStorage(values)
}
} catch (err) {
const msg = err.message || err.toString?.()
// handle errors from JIT invoices by ignoring them
if (msg === 'modal closed' || msg === 'invoice canceled') return
toaster.danger('submit error:' + msg)
}
}, [onSubmit, feeButton?.total, toaster, clearLocalStorage, storageKeyPrefix])
return (
{children}
)
}
export function Select ({ label, items, info, groupClassName, onChange, noForm, overrideValue, hint, ...props }) {
const [field, meta, helpers] = noForm ? [{}, {}, {}] : useField(props)
const formik = noForm ? null : useFormikContext()
const invalid = meta.touched && meta.error
useEffect(() => {
if (overrideValue) {
helpers.setValue(overrideValue)
}
}, [overrideValue])
return (
{
if (field?.onChange) {
field.onChange(e)
}
if (onChange) {
onChange(formik, e)
}
}}
isInvalid={invalid}
>
{items.map(item => {
if (item && typeof item === 'object') {
return (
)
} else {
return
}
})}
{info && {info}}
{meta.touched && meta.error}
{hint &&
{hint}
}
)
}
export function DatePicker ({ fromName, toName, noForm, onChange, when, from, to, className, ...props }) {
const formik = noForm ? null : useFormikContext()
const [,, fromHelpers] = noForm ? [{}, {}, {}] : useField({ ...props, name: fromName })
const [,, toHelpers] = noForm ? [{}, {}, {}] : useField({ ...props, name: toName })
const { minDate, maxDate } = props
const [[innerFrom, innerTo], setRange] = useState(whenRange(when, from, to))
useEffect(() => {
setRange(whenRange(when, from, to))
if (!noForm) {
fromHelpers.setValue(from)
toHelpers.setValue(to)
}
}, [when, from, to])
const dateFormat = useMemo(() => {
const now = new Date(2013, 11, 31)
let str = now.toLocaleDateString()
str = str.replace('31', 'dd')
str = str.replace('12', 'MM')
str = str.replace('2013', 'yy')
return str
}, [])
const innerOnChange = ([from, to], e) => {
if (from) {
from = new Date(new Date(from).setHours(0, 0, 0, 0))
}
if (to) {
to = new Date(new Date(to).setHours(23, 59, 59, 999))
}
setRange([from, to])
if (!noForm) {
fromHelpers.setValue(from)
toHelpers.setValue(to)
}
if (!from || !to) return
onChange?.(formik, [from, to], e)
}
const onChangeRawHandler = (e) => {
// raw user data can be incomplete while typing, so quietly bail on exceptions
try {
const dateStrings = e.target.value.split('-', 2)
const dates = dateStrings.map(s => new Date(s))
let [from, to] = dates
if (from) {
from = new Date(from.setHours(0, 0, 0, 0))
if (minDate) from = new Date(Math.max(from.getTime(), minDate.getTime()))
try {
if (to) {
to = new Date(to.setHours(23, 59, 59, 999))
if (maxDate) to = new Date(Math.min(to.getTime(), maxDate.getTime()))
}
// if end date isn't valid, set it to the start date
if (!(to instanceof Date && !isNaN(to)) || to < from) to = new Date(from.setHours(23, 59, 59, 999))
} catch {
to = new Date(from.setHours(23, 59, 59, 999))
}
innerOnChange([from, to], e)
}
} catch { }
}
return (
)
}
function Client (Component) {
return ({ initialValue, ...props }) => {
// This component can be used for Formik fields
// where the initial value is not available on first render.
// Example: value is stored in localStorage which is fetched
// after first render using an useEffect hook.
const [,, helpers] = useField(props)
useEffect(() => {
helpers.setValue(initialValue)
}, [initialValue])
return
}
}
export const ClientInput = Client(Input)
export const ClientCheckbox = Client(Checkbox)