formula-one
formula-one copied to clipboard
Implement navigation protection
Formula One should make it easy to implement navigation protection.
If a form has been edited, but not saved, a form should prompt before allowing navigation away from the form (e.g. via a url change or via clicking a link).
Will this do? @dmnd @zgotsch
class SimpleExample extends React.Component<Props, State> {
constructor(props) {
super(props);
this.formRef = React.createRef();
this.navigationListener = () => {
const {pristine, submitted} = this.formRef.current.state;
if (pristine || submitted) {
return;
}
// do something
};
}
componentDidMount() {
window.addEventListener("popstate", this.navigationListener);
}
componentWillUnmount() {
window.removeEventListener("popstate", this.navigationListener);
}
render() {
return (
<div>
<Form initialValue={123} ref={this.formRef} />
</div>
);
}
}
Or just
type Props = {
...
onNavigate: (formState) => void
}
<Form onNavigate={/* do something */} />
Ideally a Formula One user doesn't need to reach into F1 state like this.formRef.current.state
.
I haven't thought about what this API should look like, but maybe you'll get some ideas from this beforeNavigate
thing we have in the flexport repo:
/**
* TEAM: frontend_infra
* WATCHERS: dounan
*
* @flow
*/
export type Delegate = {
// if shouldConfirm returns true, a confirmation modal will appear when the user tries to navigate away
//
// newUrl: the url the user is trying to navigate to, or null if the user is trying to close the tab
+shouldConfirm: (newUrl: string | null) => boolean,
// willNavigate will be called when the user discards the modal and the router is about to navigate away
+willNavigate: () => void,
};
// Doesn't cover all cases, but enough for our usage.
function isFunction(f: any): boolean {
return typeof f === "function";
}
function canRegister(delegate: Delegate): boolean {
if (process.env.NODE_ENV !== "production") {
return (
delegate &&
isFunction(delegate.shouldConfirm) &&
isFunction(delegate.willNavigate)
);
}
return true;
}
class BeforeNavigate {
delegates: Array<Delegate> = [];
register(delegate: Delegate): void {
if (!canRegister(delegate)) {
throw new Error("Invalid delegate");
}
this.delegates.push(delegate);
}
unregister(delegate: Delegate): void {
const idx: number = this.delegates.indexOf(delegate);
if (idx > -1) {
this.delegates.splice(idx, 1);
}
}
shouldConfirm(newUrl: string | null): boolean {
return this.delegates.some(d => d.shouldConfirm(newUrl));
}
willNavigate(): void {
this.delegates.forEach(d => d.willNavigate());
}
}
// Share a global instance
export default new BeforeNavigate();
In our app the router subscribes to popstate
(etc) and via beforeNavigate
asks all current forms if it's safe to navigate. So we'll want something similar for Formula One, but hopefully generic.
- Maybe F1 provides a function that a router can call to learn if a form is dirty
- Maybe an F1 callback like
onValidation
can proactively tell a router that a form is dirty all the time, and then the router only looks at internal state to figure out if it's safe to navigate.
Sorry I haven't thought through how this would work: this issue definitely needs design.