Jibbedi / react-wizard-primitive

A react wizard primitive - built with hooks!

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Zero dependencies scaffold for a wizard / stepper without any UI restrictions. Hooks and render props API available!

Build Coverage License Version Types Size Dependencies Pull Requests welcome Downloads semantic-release

github stars


The Problem

You need to implement a wizard / stepper, but have specific UI requirements. You want a flexible solution that suits a wide range of use cases. Check out the examples to see what's possible.

The Solution

React Wizard Primitive handles the state management and you bring the UI. Leverage a render props or hooks API to get rid of the tedious boilerplate. You can use this library to build other abstractions, that better suit your specific needs on top of it.

Quick Start

  1. Install react-wizard-primitive
npm i react-wizard-primitive
  1. Create your first wizard
Hooks API
import React from "react";
import { useWizard } from "react-wizard-primitive";

export default function App() {
  const { getStep, nextStep } = useWizard();
  const stepTitles = ["First", "Second", "Third"]; //let's render them one after the other

  return (
    <div>
      {stepTitles.map(
        (stepTitle) =>
          getStep().isActive && <div onClick={nextStep}>{stepTitle}</div>
      )}
    </div>
  );
}

See a working example here.

Render Props API
import React from "react";
import { Wizard } from "react-wizard-primitive";

export default function App() {
  const stepTitles = ["First", "Second", "Third"]; //let's render them one ofter the other

  return (
    <Wizard>
      {({ getStep, nextStep }) => (
        <div>
          {stepTitles.map(
            (stepTitle) =>
              getStep().isActive && <div onClick={nextStep}>{stepTitle}</div>
          )}
        </div>
      )}
    </Wizard>
  );
}

See a working example here.

Splitting the wizard in multiple components

Using the getStep function is great if you are creating a small wizard which can live inside a single component.

If your wizard grows it can come in handy to separate each step inside it's own component. react-wizard-primitive makes this really easy.

  1. Put a Wizard Component as your wizard root
  2. Create Step components. All step components inside one Wizard Component work together.
Hooks API
import React from "react";
import { Wizard, useWizardStep } from "react-wizard-primitive";

const FirstStep = () => {
  const { isActive, nextStep } = useWizardStep();
  return isActive ? <div onClick={nextStep}>First Step</div> : null;
};

const SecondStep = () => {
  const { isActive, nextStep } = useWizardStep();
  return isActive ? <div onClick={nextStep}>Second Step</div> : null;
};

export default function App() {
  return (
    <Wizard>
      <FirstStep />
      {/* a step doesn't need to be a direct child of the wizard. It can be nested inside of html or react components, too!*/}
      <div>
        <SecondStep />
      </div>
    </Wizard>
  );
}

See a working example here.

Render Props API
import React from "react";
import { Wizard, WizardStep } from "react-wizard-primitive";

const FirstStep = () => {
  return (
    <WizardStep>
      {({ isActive, nextStep }) =>
        isActive ? <div onClick={nextStep}>First Step</div> : null
      }
    </WizardStep>
  );
};

const SecondStep = () => {
  return (
    <WizardStep>
      {({ isActive, nextStep }) =>
        isActive ? <div onClick={nextStep}>Second Step</div> : null
      }
    </WizardStep>
  );
};

export default function App() {
  return (
    <Wizard>
      <FirstStep />
      {/* a step doesn't need to be a direct child of the wizard. It can be nested inside of html or react components, too!*/}
      <div>
        <SecondStep />
      </div>
      {/* WizardStep can also be used without placing it inside another component*/}
      <WizardStep>
        {({ isActive }) => (isActive ? <div>Third Step</div> : null)}
      </WizardStep>
    </Wizard>
  );
}

See a working example here.

Building your own abstractions

Sometimes you need a wizard in multiple places, but keep the styling consistent. react-wizard-primitive provides you with basic building blocks that you can use to build powerful abstractions on top of it.

<MyCustomWizard>
  <MyCustomWizard.Step>
    <TextFields />
  </MyCustomWizard.Step>
  <MyCustomWizard.Step>
    <div>Just some other inline jsx</div>
  </MyCustomWizard.Step>
  <MyCustomWizard.Step>
    <div>And another one</div>
  </MyCustomWizard.Step>
  <MyCustomWizard.Step>
    <div>Last one</div>
  </MyCustomWizard.Step>
</MyCustomWizard>

See a working example here.

API

Step

A step is the main data structure for the wizard. It is returned by the getStep call and provided by useWizardStep and the WizardStep component.

  • index number

    The index of the current step

  • isActive boolean

    Is the state the currently active one?

  • hasBeenActive boolean

    Has the step been active before?

  • nextStep function

    Move to the step after this step.

  • previousStep function

    Move to the step before this step.

  • resetToStep function(stepIndex : number, options? : {skipOnChangeHandler?: boolean})

    Move to step with index stepIndex. Set hasBeenActive for all following steps as well as the new step to false. You can pass in options to control if the onChange handler should be called for this operation.

  • moveToStep function(stepIndex : number, options? : {skipOnChangeHandler?: boolean})

    Set this step to be currently active. All following steps will keep the activated state. You can pass in options to control if the onChange handler should be called for this operation. moveToStep is an alias of goToStep.

  • goToStep function(stepIndex : number, options? : {skipOnChangeHandler?: boolean})

    Go to the step with the given index. You can pass in options to control if the onChange handler should be called for this operation.

useWizard

A hook that manages the state of the wizard and provides you with functions to interact with it

Arguments

  • options object (optional)

    • initialStepIndex number (optional)

      The provided step index will be displayed initially. All previous steps will be treated as if they've been already activated.

    • onChange function({newStepIndex : number, previousStepIndex: number, maxActivatedStepIndex : number}) (optional)

      Is called every time the wizard step changes.

Returns

  • wizard object
    • getStep function(options?) : Step

      Creates a wizard step and provides it's current state. It can take an optional options object, which can take a routeTitle See routing for more details.

    • activeStepIndex number

      Currently active step

    • maxActivatedStepIndex number

      Index of the furthest step, that has been activated

    • nextStep function

      Call this to proceed to the next step

    • previousStep function

      Call this to proceed to the previous step

    • resetToStep function(stepIndex : number, options? : {skipOnChangeHandler?: boolean})

      Move to step with index stepIndex. Set hasBeenActive for all following steps as well as the new step to false. You can pass in options to control if the onChange handler should be called for this operation.

    • moveToStep function(stepIndex : number, options? : {skipOnChangeHandler?: boolean})

      Move to step with index stepIndex. You can pass in options to control if the onChange handler should be called for this operation. moveToStep is an alias of goToStep.

    • goToStep function(stepIndex : number, options? : {skipOnChangeHandler?: boolean})

      Go to the step with the given index. You can pass in options to control if the onChange handler should be called for this operation.

Example

// start at third step and log every change
const { getStep } = useWizard({
  initialStepIndex: 2,
  onChange: ({ newStepIndex, previousStepIndex }) => {
    console.log(`I moved from step ${previousStepIndex} to ${newStepIndex}`);
  },
});

useWizardStep

A hook that let's you split your wizard into separate components and creates a wizard step. It calls getStep under the hood.

Arguments

  • options WizardStepOptions (optional)

    It can take an optional options object, which can take a routeTitle See routing for more details.

Returns

Example

// isActive will be true if this wizardStep should be rendered, nextStep will move to the next step
const { isActive, nextStep } = useWizardStep();

Wizard

A component that servers as the root for a wizard if you choose to split your wizard into multiple components.

Otherwise it can be used as a replacement for the useWizard hook. It takes the same arguments (as props) and returns the same values to the render prop.

Example

// start at third step and log every change
<Wizard initialStepIndex="2" onChange={({newStepIndex, previousStepIndex}) => {
  console.log(`I moved from step ${previousStepIndex} to ${newStepIndex}`);
}}>
{
  ({getStep}) => {
    ...
  }
}
</Wizard>

WizardStep

A component that serves as an alternative to the useWizardStep hook. It takes the same arguments (as props) and returns the same values to the render prop.

Example

// isActive will be true if this wizardStep should be rendered, nextStep will move to the next step
<WizardStep>
{
  ({isActive, nextStep}) => {
    ...
  }
}
</WizardStep>

Routing

Basics

Out of the box react-wizard-primitive supports an opt-in routing via hash.

In order to use it, you need to specify a routeTitle in the getStep call or pass it as a prop to the WizardStep or useWizardStep hook. The routeTitle will be used as the hash.

If no routeTitle is provided, react-wizard-primitive won't make any changes to the URL. If only some steps are provided with a title, we assume that this happened by mistake, and won't change the url either. Instead we log a warning to the console, indicating which steps are missing a title.

Initial Hash Route

If a hash is present when the wizard is first rendered, it will try to find a matching step to that hash and jump to it or otherwise jump to the initial step.

You can use this behaviour to start the wizard at any given point.

Example

<Wizard>
  {"yourdomain.com/#/first-step"}
  <WizardStep routeTitle="first-step">
    {({ isActive, nextStep }) =>
      isActive && <div onClick={nextStep}>Step 1</div>
    }
  </WizardStep>

  {"yourdomain.com/#/second-step"}
  <WizardStep routeTitle="second-step">
    {({ isActive, nextStep }) =>
      isActive && <div onClick={nextStep}>Step 2</div>
    }
  </WizardStep>

  {"yourdomain.com/#/third-step"}
  <WizardStep routeTitle="third-step">
    {({ isActive, nextStep }) =>
      isActive && <div onClick={nextStep}>Step 3</div>
    }
  </WizardStep>
</Wizard>

Examples

You can build nearly anything on top of react-wizard-primitive. Take a look at those examples to get an idea of what's possible.

This is a good starting point, if you want to see a basic hook implementation. A classical wizard, which displays the steps one after the other.

Basic Example

Same example, but implemented with the render props API.

This example demonstrates, how you can build a wizard that displays the steps one after another, but keeps the already displayed steps around.

Buildup Wizard

It can get tedious to work with the basic building blocks and repeat styling or display handling all over again. This example demonstrates how you can build your own abstractions on top of react-wizard-primitive.

<MyCustomWizard>
  <MyCustomWizard.Step>
    <TextFields />
  </MyCustomWizard.Step>
  <MyCustomWizard.Step>
    <div>Just some other inline jsx</div>
  </MyCustomWizard.Step>
  <MyCustomWizard.Step>
    <div>And another one</div>
  </MyCustomWizard.Step>
  <MyCustomWizard.Step>
    <div>Last one</div>
  </MyCustomWizard.Step>
</MyCustomWizard>

Buildup Wizard

Migration from older versions

Upgrading from v1

  • hasBeenActive is now false on first render. To achieve the previous behavior you can modify your code to hasBeenActive || isActive
  • maxVisitedStepIndex has been renamed to maxActivatedStepIndex and will not include the currently active step if it's first rendered. To achieve the previous behavior you can modify your code to Math.max(maxActivatedStepIndex, activeStepIndex)

Contributors


Johannes Kling

πŸ’» πŸ“– πŸ€” πŸ’‘ ⚠️

Jose Miguel Bejarano

πŸ€”

kaYcee

πŸ€”

Kevin Aldebert

πŸ€”

Carlos Santos

πŸ›

Andrei Benea

πŸ›

Uzwername

πŸ“–

About

A react wizard primitive - built with hooks!

License:MIT License


Languages

Language:TypeScript 97.8%Language:JavaScript 2.2%