# Stepper

Dynamic stepper plugin that guides users through the steps of a task.

[![npm](https://img.shields.io/badge/npm-v4.2.0-blue)](https://www.npmjs.com/package/@preline/stepper) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) [![Demo](https://img.shields.io/badge/demo-online-brightgreen)](https://preline.co/plugins/stepper.html)

## Contents

- [Overview](#overview)
- [Installation](#installation)
- [Basic usage](#basic-usage)
- [Configuration Options](#configuration-options)
- [JavaScript API](#javascript-api)
- [Events](#events)
- [Common Patterns](#common-patterns)
- [License](#license)

## Overview

The Stepper component provides a multi-step wizard interface that guides users through a sequential process. It supports both linear (sequential) and non-linear (jumpable) modes, with options for optional steps, error states, and completion tracking.

**Key Features:**
- Multi-step wizard interface
- Linear and non-linear navigation modes
- Optional steps support
- Error and processing states
- Completion tracking
- Programmatic control via JavaScript API
- Event system for step transitions
- Accessibility attributes (ARIA) built-in

## Installation

To get started, install Stepper plugin via npm, else you can skip this step if you are already using Preline UI as a package.

```bash
npm i @preline/stepper
```

### CSS

Use [`@source`](https://tailwindcss.com/docs/functions-and-directives#source-directive) to register the plugin's JavaScript path for Tailwind CSS scanning, then [`@import`](https://tailwindcss.com/docs/functions-and-directives#import-directive) the plugin's CSS files into your Tailwind CSS file.

```css
@import "tailwindcss";

/* @preline/stepper */
@source "../node_modules/@preline/stepper/*.js";
@import "./node_modules/@preline/stepper/variants.css";
@import "./node_modules/@preline/stepper/theme.css";
```

### JavaScript

Include the JavaScript that powers the interactive elements near the end of your `</body>` tag:

```html
<script src="./node_modules/@preline/stepper/index.js"></script>
```

### Manual Initialization

Use the `non-auto` entry if you need manual initialization. In this mode, automatic initialization on page load is not included, so the component should be initialized explicitly.

```html
<script type="module">
  import HSStepper from "@preline/stepper/non-auto.mjs";

  new HSStepper(document.querySelector("#stepper"));
</script>
```

### Via Bundler

When using a bundler (Vite, webpack, etc.), import the plugin directly as an ES module.

`@preline/stepper` is the auto-init entry: it scans the DOM and initializes matching elements automatically.

```js
import "@preline/stepper";
```

`@preline/stepper/non-auto` is the manual entry: use it when you want explicit control over when initialization happens, either via `autoInit()` or by creating a specific instance yourself.

```js
import HSStepper from "@preline/stepper/non-auto";

HSStepper.autoInit();

// Or initialize a specific element manually
const el = document.querySelector("#stepper");
if (el) new HSStepper(el);
```

### TypeScript

This package ships with TypeScript type definitions. No additional `@types/` package is needed.

## Basic usage

The following example demonstrates the minimal HTML structure required for a stepper component. This is a base template without custom styling - you can apply your own CSS classes and styles as needed. The example shows three steps with navigation buttons.

```html
<div data-hs-stepper>
  <div class="hs-stepper-active:text-blue-500 hs-stepper-success:text-blue-500" data-hs-stepper-nav-item='{
    "index": 1
  }'>
    1 Step
  </div>
  <div class="hs-stepper-active:text-blue-500 hs-stepper-success:text-blue-500" data-hs-stepper-nav-item='{
    "index": 2
  }'>
    2 Step
  </div>
  <div class="hs-stepper-active:text-blue-500 hs-stepper-success:text-blue-500" data-hs-stepper-nav-item='{
    "index": 3
  }'>
    3 Step
  </div>
  
  <div data-hs-stepper-content-item='{
    "index": 1
  }' style="display: none;">
    First content.
  </div>
  <div data-hs-stepper-content-item='{
    "index": 2
  }' style="display: none;">
    Second content.
  </div>
  <div data-hs-stepper-content-item='{
    "index": 3
  }' style="display: none;">
    Third content.
  </div>
  <div data-hs-stepper-content-item='{
    "isFinal": true
  }' style="display: none;">
    Final content.
  </div>
  
  <button class="hs-stepper-disabled:opacity-50" type="button" data-hs-stepper-back-btn>
    Back
  </button>
  <button type="button" data-hs-stepper-skip-btn style="display: none;">
    Skip
  </button>
  <button type="button" data-hs-stepper-next-btn>
    Next
  </button>
  <button type="button" data-hs-stepper-finish-btn style="display: none;">
    Finish
  </button>
  <button type="reset" data-hs-stepper-reset-btn style="display: none;">
    Reset
  </button>
</div>
```

**Structure Requirements:**
- `data-hs-stepper`: Required on the container element
- `data-hs-stepper-nav-item`: Required on each navigation item
- `data-hs-stepper-content-item`: Required on each content panel
- `data-hs-stepper-back-btn`: Optional back button
- `data-hs-stepper-next-btn`: Optional next button
- `data-hs-stepper-finish-btn`: Optional finish button
- `data-hs-stepper-skip-btn`: Optional skip button
- `data-hs-stepper-reset-btn`: Optional reset button

**Initial State:**
- First step is active by default (index 1)
- Content items should have `style="display: none;"` except the first one
- Navigation buttons visibility is controlled by the plugin

## Configuration Options

### Data Options

Data options are specified in the `data-hs-stepper`, `data-hs-stepper-nav-item`, and `data-hs-stepper-content-item` attributes.

| Attribute | Target Element | Type | Default | Description |
| --- | --- | --- | --- | --- |
| `data-hs-stepper` | Container | - | - | Activate a Stepper by specifying on an element. Should be added to the container. |
| `:currentIndex` | Inside `data-hs-stepper` | number | `1` | Index of the current step. This must be a number between 1 and the maximum number of steps. |
| `:mode` | Inside `data-hs-stepper` | `"linear"` \| `"non-linear"` | `"linear"` | The mode of the stepper. In `"non-linear"` mode, the user can navigate to any step and nav items are clickable. In `"linear"` mode, the user can only navigate to the next/back step. |
| `:isCompleted` | Inside `data-hs-stepper` | boolean | `false` | Whether the stepper is completed. |
| `data-hs-stepper-nav-item` | Navigation item | - | - | Activate a Stepper Nav Item by specifying on an element. Should be added to the nav item. |
| `:index` | Inside `data-hs-stepper-nav-item` | number | - | The index of the step to which the item belongs. This must be a number between 1 and the maximum number of steps. |
| `:isOptional` | Inside `data-hs-stepper-nav-item` | boolean | `false` | Whether the step is optional. |
| `:isCompleted` | Inside `data-hs-stepper-nav-item` | boolean | `false` | Whether the step is completed. |
| `:isSkip` | Inside `data-hs-stepper-nav-item` | boolean | `false` | Whether the step is skipped. |
| `:hasError` | Inside `data-hs-stepper-nav-item` | boolean | `false` | Whether the step has an error. |
| `data-hs-stepper-content-item` | Content item | - | - | Activate a Stepper Content Item by specifying on an element. Should be added to the content item. |
| `:index` | Inside `data-hs-stepper-content-item` | number | - | The index of the step to which the item belongs. This must be a number between 1 and the maximum number of steps. |
| `:isCompleted` | Inside `data-hs-stepper-content-item` | boolean | `false` | Whether the step is completed. |
| `:isSkip` | Inside `data-hs-stepper-content-item` | boolean | `false` | Whether the step is skipped. |
| `:isFinal` | Inside `data-hs-stepper-content-item` | boolean | `false` | Whether the step is final. |

### Tailwind Modifiers

| Name | Description |
| --- | --- |
| `hs-stepper-active:*` | Modifies the active step. |
| `hs-stepper-success:*` | Modifies the completed step. |
| `hs-stepper-disabled:*` | Modifies the "back" button when the very first step is active. |
| `hs-stepper-skipped:*` | Modifies the skipped step. |
| `hs-stepper-error:*` | Modifies the step that has error. Error class should be added manually by some event. E.g. after form validation. |
| `hs-stepper-process:*` | Modifies the step that processing. Process class should be added manually by some event. E.g. after form submit. |
| `hs-stepper-completed:*` | Modifies all steps are completed. |

## JavaScript API

The `HSStepper` object is available in the global `window` object after the plugin is loaded.

### Instance Methods

These methods are called on a stepper instance.

| Method | Parameters | Return Type | Description |
| --- | --- | --- | --- |
| `setProcessedNavItem(n)` | `n`: `number` | `void` | Set the nav item as processed. `n` is the index of the step to which the item belongs. |
| `setErrorNavItem(n)` | `n`: `number` | `void` | Set the nav item as error. `n` is the index of the step to which the item belongs. |
| `unsetProcessedNavItem(n)` | `n`: `number` | `void` | Unset the nav item as processed. `n` is the index of the step to which the item belongs. |
| `goToNext()` | None | `number` | Go to the next step. Returns the index of the next step. If the current step is the last, returns the index of the current step. |
| `goToFinish()` | None | `number` | Go to the finish step. Returns the index of the finish step. If the current step is the last, returns the index of the current step. |
| `disableButtons()` | None | `void` | Disable the "next" and "back" buttons. |
| `enableButtons()` | None | `void` | Enable the "next" and "back" buttons. |
| `destroy()` | None | `void` | Destroys the stepper instance, removes all generated markup, classes, and event listeners. Use when removing stepper from DOM. |

### Static Methods

These methods are called directly on the `HSStepper` class.

| Method | Parameters | Return Type | Description |
| --- | --- | --- | --- |
| `HSStepper.getInstance(target, isInstance)` | `target`: `HTMLElement \| string` (CSS selector)<br>`isInstance`: `boolean` (optional) | `HSStepper \| { id: string \| number, element: HSStepper } \| null` | Returns the stepper instance associated with the target. If `isInstance` is `true`, returns collection item object `{ id, element }` where `element` is the `HSStepper` instance. If `isInstance` is `false` or omitted, returns the `HSStepper` instance directly. Returns `null` if stepper instance is not found. |

### Usage Examples

**Example 1: Using instance methods (public API)**
```javascript
// Create a new stepper instance
const stepper = new HSStepper(document.querySelector('#hs-stepper'));
let errorState = 1;

stepper.on('beforeNext', (index) => {
  if (index === 2) {
    stepper.setProcessedNavItem(index);
    
    setTimeout(() => {
      stepper.unsetProcessedNavItem(index);
      stepper.enableButtons();
      
      if (errorState) {
        stepper.goToNext();
      } else {
        stepper.setErrorNavItem(index);
      }
      
      errorState = !errorState;
    }, 2000);
  }
});
```

**Example 2: Getting instance and using methods (recommended pattern)**
```javascript
// Get the stepper instance
const instance = HSStepper.getInstance('#hs-stepper', true);

if (instance) {
  const { element } = instance; // element is the HSStepper instance
  let errorState = 1;

  element.on('beforeNext', (index) => {
    if (index === 2) {
      element.setProcessedNavItem(index);
      
      setTimeout(() => {
        element.unsetProcessedNavItem(index);
        element.enableButtons();
        
        if (errorState) {
          element.goToNext();
        } else {
          element.setErrorNavItem(index);
        }
        
        errorState = !errorState;
      }, 2000);
    }
  });

  // Clean up when removing from DOM
  function removeStepper() {
    element.destroy();
  }
}
```

**Example 3: Programmatic navigation**
```javascript
const instance = HSStepper.getInstance('#hs-stepper', true);
if (instance) {
  const { element } = instance;
  // Navigate to next step
  const nextIndex = element.goToNext();
  console.log('Moved to step:', nextIndex);
  // Navigate to finish
  const finishIndex = element.goToFinish();

  console.log('Finished at step:', finishIndex);
}
```

**Example 4: Destroying stepper instance**
```javascript
const instance = HSStepper.getInstance('#hs-stepper', true);

if (instance) {
  const { element } = instance;
  const removeBtn = document.querySelector('#hs-remove-btn');

  removeBtn.addEventListener('click', () => {
    // Clean up before removing from DOM
    element.destroy();
    // Now safe to remove the element
    document.querySelector('#hs-stepper').remove();
  });
}
```

## Events

Stepper instances emit events that can be listened to for step transitions and custom behavior.

| Event Name | When Fired | Callback Parameter | Description |
| --- | --- | --- | --- |
| `on:active` | When the "active" class is set up | `currentIndex` (number) | Fires when a step becomes active. |
| `on:beforeNext` | Before the "next" button is clicked | `currentIndex` (number) | Fires before navigating to the next step. Can be used to validate or prevent navigation. |
| `on:beforeFinish` | Before the "finish" button is clicked | `currentIndex` (number) | Fires before finishing the stepper. Can be used to validate or prevent completion. |
| `on:next` | When the "next" button is clicked | `currentIndex` (number) | Fires when navigating to the next step. |
| `on:back` | When the "back" button is clicked | `currentIndex` (number) | Fires when navigating to the previous step. |
| `on:complete` | When the "complete" button is clicked | `currentIndex` (number) | Fires when completing a step. |
| `on:finish` | When the "finish" button is clicked | `currentIndex` (number) | Fires when finishing the stepper. |

### Event Usage Example

```javascript
// Get stepper instance
const instance = HSStepper.getInstance('#hs-stepper', true);

if (instance) {
  const { element } = instance;

  // Listen to step activation
  element.on('active', (currentIndex) => {
    console.log('Step activated:', currentIndex);
    // Perform actions when step becomes active
    // e.g., load content, initialize form fields
  });

  // Listen to before next (for validation)
  element.on('beforeNext', (currentIndex) => {
    console.log('Before next step:', currentIndex);
    // Validate current step before proceeding
    // Can prevent navigation if validation fails
  });

  // Listen to finish
  element.on('finish', (currentIndex) => {
    console.log('Stepper finished:', currentIndex);
    // Perform final actions
    // e.g., submit form, show success message
  });
}
```

## Common Patterns

### Pattern 1: Non-linear Mode

Allow users to jump to any step.

```html
<div data-hs-stepper='{
  "mode": "non-linear"
}'>
  <!-- Stepper content -->
</div>
```

### Pattern 2: Error Handling

Set error state on a step after validation fails.

```html
<div id="hs-stepper-first" data-hs-stepper>
  <!-- Stepper content -->
</div>

<script>
  const instance = HSStepper.getInstance('#hs-stepper-first', true);
  
  if (instance) {
    const { element } = instance;

    // Validate and set error if needed
    function validateStep(stepIndex) {
      // Validation logic
      if (validationFails) {
        element.setErrorNavItem(stepIndex);
      }
    }
  }
</script>
```

## License

Copyright (c) 2024 Htmlstream.

Licensed under the [MIT License](https://opensource.org/licenses/MIT).
