formula-one icon indicating copy to clipboard operation
formula-one copied to clipboard

Implement navigation protection

Open zgotsch opened this issue 6 years ago • 2 comments

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).

zgotsch avatar Dec 17 '18 23:12 zgotsch

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 */} />

alexneverpo avatar Aug 15 '19 07:08 alexneverpo

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.

dmnd avatar Aug 20 '19 04:08 dmnd