americanexpress / react-albus

✨ React component library for building declarative multi-step flows.
Apache License 2.0
1.1k stars 89 forks source link

How to prevent user from accessing a particular step if wizard is tied to the router? #25

Closed puopg closed 4 years ago

puopg commented 6 years ago

Imagine this use case.

We have 10 steps in this wizard, and we should only be able to get to Step N, given Steps 1 -> N have been completed.

If this wizard is tied to the router, the browser has the previous history of where the user has been. Which means, they can get to a step via a route on their browsers history stack by hitting the back button lets say.

So this can be solved by fixing the route or fixing the current step. If we are currently rendering the Step-3 Component, and we try to move to Step-4, but Step-3 is not yet complete, we would essentially just move back to Step-3. So cool this is great!

But what it causes is a remount of the Step-3 component. Is there a better way to handle an edge case like this? Thanks!

jackjocross commented 6 years ago

That is a good use case! I want to make sure I understand where you are checking if Step-3 is complete, is it somewhere in your Step-4 component, or in an onNext function you are passing to your Wizard?

I put together this codesandbox for how I understand the use case currently. If you could fork that sandbox and update any parts I didn't catch that would be a huge help to understand if and where we need to make any changes.

puopg commented 6 years ago

Well so I think the flow would be something like:

  1. User lands on page that corresponds to Step 4.
  2. Wizard Component Mounts
  3. Wizard Component sees that user has filled out data for Steps 1 & 2, but not yet 3.
  4. Wizard Component fixes the flow by switching to Step 3.
  5. User hits back button, which would navigate the browser to Step 4.
  6. Wizard Component or the Step Component would see that we cannot be on this step, so go back to Step 3.

So to answer the question, it could be either I guess? The Wizard at the high level could be the one who manages the validation, or the step could as well. The main point is that control of the wizard when tied to the route is now given to the browser (hence the back button). Edge case i know, but just curious how one would handle that situation.

Like I mentioned, it's no issue to redirect back to a step that we can actually be on. But doing so without unmounting a component would be cool if possible.

jackjocross commented 6 years ago

Ah okay now I see what you're saying. So I think in the specific flow you outlined one option might be to use replace instead of push when fixing the flow by switching to Step 3. That way the back button would not navigate the browser to Step 4. This codesandbox shows that behavior.

But for the general use case maybe it would be useful to have onNext run for browser history changes in addition to next being called. I think that would allow you to make some decisions before unmounting the current step.

I don't think running onNext would make sense for browser back though so I need to think about that use case a bit more.

nik-john commented 6 years ago

@puopg Did you find a satisfactory solution to your issue? I am facing the same scenario and I am looking at limiting the Steps I pass into the Steps HOC via some filtering logic like so:

Consider a 5 stage wizard with steps 'a...e'. Whenever a step has been completed, its corresponding data is stored in my Redux store which looks like this:

store: {
    a : [....],
    b: [....],
    .....
}

My business logic is:

puopg commented 6 years ago

@nik-john Yea your logic is basically what I wanted. If you break a wizard flow into steps, in order to be on any step, all steps prior must be complete. Otherwise you fall back to the most completed step possible.

What I have done so far is I created a component called the StepValidator, which basically wraps the Steps so like this:

<StepValidator wizard={wizard} validators={this.getValidators()}>
     <Steps>
        <Step id="connect" name="connect"}>
             <GroupingStep next={next}/>
        </Step>

        <Step id="confirm" name="confirm">
              <ConfirmationStep next={next} />
         </Step>
     </Steps>
</StepValidator>

Where it receives an array of validator functions that can be invoked and they all return a boolean value. Then based on what the wizard sees as the first step, we identify which was the first validator to fail.

So for a set of 3 steps, A, B, and C

The component abstraction is for render control. In the case where validation fails, we want to return nothing while we clean up and change the wizard to the correct step. Once the correct step is reached, our validation will be passing, and we allow the children to render.

nik-john commented 6 years ago

That's a pretty good idea. I came up with a similar one, wherein I created the validators on my selectors file. I'll update the progress here

jackjocross commented 6 years ago

Nice! I think composition is definitely the way to go for this use case.