Skip to content

Commit

Permalink
Support a before-unload prompt on forms (#151)
Browse files Browse the repository at this point in the history
* Add latest react router

* Add useprompt feature

* Fix Merge duplicates

---------

Co-authored-by: John Cherry <github@johncherry.me>
  • Loading branch information
Jbithell and cherry-john authored Jul 14, 2023
1 parent 3750d5d commit 22552d4
Show file tree
Hide file tree
Showing 4 changed files with 54 additions and 0 deletions.
4 changes: 4 additions & 0 deletions src/app/Pages/Admin/Faders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { useAppSelector } from './../../apis/redux/mainStore'
import { DatabaseFader } from './../../../database/repository/fader'
import { ApiCall } from './../../apis/wrapper'
import { showNotification } from '@mantine/notifications'
import { usePrompt } from '../../apis/utilities/usePrompt'

interface FormValues {
faders: Array<DatabaseFader>
Expand Down Expand Up @@ -120,6 +121,8 @@ export const FadersConfigurationPage = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [faders])
const saveByUserNeeded = formOriginalValues !== JSON.stringify(form.values) // Does the user have unsaved changes
usePrompt(saveByUserNeeded ? 'You have unsaved changes, are you sure you want to leave this page?' : false)

// Handle the submit button
const handleSubmit = (values: FormValues) => {
setLoadingOverlayVisible(true)
Expand All @@ -133,6 +136,7 @@ export const FadersConfigurationPage = () => {
})
})
}

const fields = form.values.faders.map((_, index) => (
<Draggable key={index} index={index} draggableId={index.toString()}>
{provided => (
Expand Down
2 changes: 2 additions & 0 deletions src/app/Pages/Admin/Folders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd'
import { ButtonIconSelectItem, availableIcons } from '../../Components/ControlPanel/ButtonIcon'
import { DatabaseFolder } from './../../../database/repository/folder'
import { useAppSelector } from './../../apis/redux/mainStore'
import { usePrompt } from './../../apis/utilities/usePrompt'
import { ApiCall } from './../../apis/wrapper'

interface FormValues {
Expand Down Expand Up @@ -88,6 +89,7 @@ export const FoldersConfigurationPage = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [folders])
const saveByUserNeeded = formOriginalValues !== JSON.stringify(form.values) // Does the user have unsaved changes
usePrompt(saveByUserNeeded ? 'You have unsaved changes, are you sure you want to leave this page?' : false)

// Handle the submit button
const handleSubmit = (values: typeof form.values) => {
Expand Down
3 changes: 3 additions & 0 deletions src/app/Pages/Admin/Presets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { FaSpaceShuttle } from '@react-icons/all-files/fa/FaSpaceShuttle'
import { FaTrash } from '@react-icons/all-files/fa/FaTrash'
import React, { useEffect, useState } from 'react'
import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd'
import { usePrompt } from '../../apis/utilities/usePrompt'
import { DatabasePreset, PresetTypes } from './../../../database/repository/preset'
import { E131PresetEditModal } from './../../Components/Admin/Controls/Presets/EditModal/E131'
import { HTTPPresetEditModal } from './../../Components/Admin/Controls/Presets/EditModal/HTTP'
Expand Down Expand Up @@ -96,6 +97,8 @@ export const PresetsConfigurationPage = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [presets])
const saveByUserNeeded = formOriginalValues !== JSON.stringify(form.values) // Does the user have unsaved changes
usePrompt(saveByUserNeeded ? 'You have unsaved changes, are you sure you want to leave this page?' : false)

// Handle the submit button
const handleSubmit = (values: typeof form.values) => {
setLoadingOverlayVisible(true)
Expand Down
45 changes: 45 additions & 0 deletions src/app/apis/utilities/usePrompt.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import * as React from 'react'
import { useBeforeUnload, unstable_useBlocker as useBlocker } from 'react-router-dom'

// From https://gist.github.com/chaance/2f3c14ec2351a175024f62fd6ba64aa6

// You can abstract `useBlocker` to use the browser's `window.confirm` dialog to
// determine whether or not the user should navigate within the current origin.
// `useBlocker` can also be used in conjunction with `useBeforeUnload` to
// prevent navigation away from the current origin.
//
// IMPORTANT: There are edge cases with this behavior in which React Router
// cannot reliably access the correct location in the history stack. In such
// cases the user may attempt to stay on the page but the app navigates anyway,
// or the app may stay on the correct page but the browser's history stack gets
// out of whack. You should test your own implementation thoroughly to make sure
// the trade offs are right for your users.

export const usePrompt = (
message: string | null | undefined | false,
{ beforeUnload }: { beforeUnload?: boolean } = {}
) => {
const blocker = useBlocker(
React.useCallback(() => (typeof message === 'string' ? !window.confirm(message) : false), [message])
)
const prevState = React.useRef(blocker.state)
React.useEffect(() => {
if (blocker.state === 'blocked') {
blocker.reset()
}
prevState.current = blocker.state
}, [blocker])

useBeforeUnload(
React.useCallback(
event => {
if (beforeUnload && typeof message === 'string') {
event.preventDefault()
event.returnValue = message
}
},
[message, beforeUnload]
),
{ capture: true }
)
}

0 comments on commit 22552d4

Please sign in to comment.