Skip to content

Commit

Permalink
Merge pull request #29 from alexzhang1030/refactor/extract
Browse files Browse the repository at this point in the history
refactor: extract common logic to core package
  • Loading branch information
damianricobelli authored Jan 7, 2025
2 parents 8b8b415 + f3f19a2 commit f8943d8
Show file tree
Hide file tree
Showing 17 changed files with 348 additions and 213 deletions.
5 changes: 5 additions & 0 deletions .changeset/afraid-hornets-cry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@stepperize/core": major
---

refactor: extract common logic to @stepprize/core
5 changes: 5 additions & 0 deletions .changeset/orange-jeans-rescue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@stepperize/react": patch
---

refactor: use @stepprize/core
29 changes: 29 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "@stepperize/core",
"version": "0.0.1",
"private": false,
"sideEffects": false,
"files": [
"dist"
],
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.js"
}
},
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"lint": "turbo lint",
"clean": "rm -rf .turbo && rm -rf node_modules dist"
},
"devDependencies": {
"tsup": "^8.3.5",
"typescript": "^5.7.2"
}
}
2 changes: 2 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export type * from "./types";
export * from "./utils";
117 changes: 117 additions & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
export type Step = { id: string } & Record<string, any>;

export type Stepper<Steps extends Step[] = Step[]> = {
/** Returns all steps. */
all: Steps;
/** Returns the current step. */
current: Steps[number];
/** Returns true if the current step is the last step. */
isLast: boolean;
/** Returns true if the current step is the first step. */
isFirst: boolean;
/** Advances to the next step. */
next: () => void;
/** Returns to the previous step. */
prev: () => void;
/** Returns a step by its ID. */
get: <Id extends Get.Id<Steps>>(id: Id) => Get.StepById<Steps, Id>;
/** Navigates to a specific step by its ID. */
goTo: (id: Get.Id<Steps>) => void;
/** Resets the stepper to its initial state. */
reset: () => void;
/**
* Executes a function based on the current step ID.
* @param id - The ID of the step to check.
* @param whenFn - Function to execute if the current step matches the ID.
* @param elseFn - Optional function to execute if the current step does not match the ID.
* @returns The result of whenFn or elseFn.
*/
when: <Id extends Get.Id<Steps>, R1, R2>(
id: Id | [Id, ...boolean[]],
whenFn: (step: Get.StepById<Steps, Id>) => R1,
elseFn?: (step: Get.StepSansId<Steps, Id>) => R2,
) => R1 | R2;
/**
* Executes a function based on a switch-case-like structure for steps.
* @param when - An object mapping step IDs to functions.
* @returns The result of the function corresponding to the current step ID.
*/
switch: <R>(when: Get.Switch<Steps, R>) => R;
/**
* Matches the current state with a set of possible states and executes the corresponding function.
* @param state - The current state ID.
* @param matches - An object mapping state IDs to functions.
* @returns The result of the matched function or null if no match is found.
*/
match: <State extends Get.Id<Steps>, R>(state: State, matches: Get.Switch<Steps, R>) => R | null;
};

export type Utils<Steps extends Step[] = Step[]> = {
/**
* Retrieves all steps.
* @returns An array of all steps.
*/
getAll: () => Steps;
/**
* Retrieves a step by its ID.
* @param id - The ID of the step to retrieve.
* @returns The step with the specified ID.
*/
get: <Id extends Get.Id<Steps>>(id: Id) => Get.StepById<Steps, Id>;
/**
* Retrieves the index of a step by its ID.
* @param id - The ID of the step to retrieve the index for.
* @returns The index of the step.
*/
getIndex: <Id extends Get.Id<Steps>>(id: Id) => number;
/**
* Retrieves a step by its index.
* @param index - The index of the step to retrieve.
* @returns The step at the specified index.
*/
getByIndex: <Index extends number>(index: Index) => Steps[Index];
/**
* Retrieves the first step.
* @returns The first step.
*/
getFirst: () => Steps[number];
/**
* Retrieves the last step.
* @returns The last step.
*/
getLast: () => Steps[number];
/**
* Retrieves the next step after the specified ID.
* @param id - The ID of the current step.
* @returns The next step.
*/
getNext: <Id extends Get.Id<Steps>>(id: Id) => Steps[number];
/**
* Retrieves the previous step before the specified ID.
* @param id - The ID of the current step.
* @returns The previous step.
*/
getPrev: <Id extends Get.Id<Steps>>(id: Id) => Steps[number];
/**
* Retrieves the neighboring steps (previous and next) of the specified step.
* @param id - The ID of the current step.
* @returns An object containing the previous and next steps.
*/
getNeighbors: <Id extends Get.Id<Steps>>(id: Id) => { prev: Steps[number] | null; next: Steps[number] | null };
};

export namespace Get {
/** Returns a union of possible IDs from the given Steps. */
export type Id<Steps extends Step[] = Step[]> = Steps[number]["id"];

/** Returns a Step from the given Steps with the given Step Id. */
export type StepById<Steps extends Step[], Id extends Get.Id<Steps>> = Extract<Steps[number], { id: Id }>;

/** Returns any Steps from the given Steps without the given Step Id. */
export type StepSansId<Steps extends Step[], Id extends Get.Id<Steps>> = Exclude<Steps[number], { id: Id }>;

/** Returns any Steps from the given Steps without the given Step Id. */
export type Switch<Steps extends Step[], R> = {
[Id in Get.Id<Steps>]?: (step: Get.StepById<Steps, Id>) => R;
};
}
75 changes: 75 additions & 0 deletions packages/core/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import type { Step, Stepper, Get, Utils } from "./types";

export function generateStepperUtils<const Steps extends Step[]>(
...steps: Steps
) {
return {
getAll() {
return steps;
},
get: (id) => {
const step = steps.find((step) => step.id === id);
return step as Get.StepById<Steps, typeof id>;
},
getIndex: (id) => steps.findIndex((step) => step.id === id),
getByIndex: (index) => steps[index],
getFirst() {
return steps[0];
},
getLast() {
return steps[steps.length - 1];
},
getNext(id) {
return steps[steps.findIndex((step) => step.id === id) + 1];
},
getPrev(id) {
return steps[steps.findIndex((step) => step.id === id) - 1];
},
getNeighbors(id) {
const index = steps.findIndex((step) => step.id === id);
return {
prev: index > 0 ? steps[index - 1] : null,
next: index < steps.length - 1 ? steps[index + 1] : null,
};
},
} satisfies Utils<Steps>;
}

export function getInitialStepIndex<Steps extends Step[]>(
steps: Steps,
initialStep?: Get.Id<Steps>
) {
return Math.max(
steps.findIndex((step) => step.id === initialStep),
0
);
}

export function generateCommonStepperUseFns<const Steps extends Step[]>(
steps: Steps,
currentStep: Steps[number],
stepIndex: number
) {
return {
switch(when) {
const whenFn = when[currentStep.id as keyof typeof when];
return whenFn?.(
currentStep as Get.StepById<typeof steps, (typeof currentStep)["id"]>
);
},
when(id, whenFn, elseFn) {
const currentStep = steps[stepIndex];
const matchesId = Array.isArray(id)
? currentStep.id === id[0] && id.slice(1).every(Boolean)
: currentStep.id === id;

return matchesId
? whenFn?.(currentStep as any)
: elseFn?.(currentStep as any);
},
match(state, matches) {
const matchFn = matches[state as keyof typeof matches];
return matchFn?.(state as any);
},
} as Pick<Stepper<Steps>, "switch" | "when" | "match">;
}
17 changes: 17 additions & 0 deletions packages/core/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"outDir": "dist",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"removeComments": false,
"module": "esnext",
"target": "esnext",
"moduleResolution": "bundler",
"esModuleInterop": true,
"skipLibCheck": true,
"strict": true
},
"include": ["."],
"exclude": ["node_modules", "dist"]
}
12 changes: 12 additions & 0 deletions packages/core/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { defineConfig } from "tsup";

export default defineConfig({
entry: ["src/index.ts"],
format: ["cjs", "esm"],
dts: true,
sourcemap: false,
clean: true,
minify: true,
treeshake: true,
tsconfig: "tsconfig.json",
});
3 changes: 3 additions & 0 deletions packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@
"prepublishOnly": "pnpm run build",
"clean": "rm -rf .turbo && rm -rf node_modules dist"
},
"dependencies": {
"@stepperize/core": "workspace:*"
},
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
Expand Down
57 changes: 5 additions & 52 deletions packages/react/src/define-stepper.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,16 @@
import type { Get, ScopedProps, Step, Stepper, StepperReturn, Utils } from "./types";
import { generateCommonStepperUseFns, generateStepperUtils, getInitialStepIndex, Step, Stepper, Get } from "@stepperize/core";
import type { ScopedProps, StepperReturn } from "./types";

import * as React from "react";

export const defineStepper = <const Steps extends Step[]>(...steps: Steps): StepperReturn<Steps> => {
const Context = React.createContext<Stepper<Steps> | null>(null);

const utils = {
getAll() {
return steps;
},
get: (id) => {
const step = steps.find((step) => step.id === id);
return step as Get.StepById<Steps, typeof id>;
},
getIndex: (id) => steps.findIndex((step) => step.id === id),
getByIndex: (index) => steps[index],
getFirst() {
return steps[0];
},
getLast() {
return steps[steps.length - 1];
},
getNext(id) {
return steps[steps.findIndex((step) => step.id === id) + 1];
},
getPrev(id) {
return steps[steps.findIndex((step) => step.id === id) - 1];
},
getNeighbors(id) {
const index = steps.findIndex((step) => step.id === id);
return {
prev: index > 0 ? steps[index - 1] : null,
next: index < steps.length - 1 ? steps[index + 1] : null,
};
},
} satisfies Utils<Steps>;
const utils = generateStepperUtils(...steps);

const useStepper = (initialStep?: Get.Id<Steps>) => {
const initialStepIndex = React.useMemo(
() =>
Math.max(
steps.findIndex((step) => step.id === initialStep),
0,
),
() => getInitialStepIndex(steps, initialStep),
[initialStep],
);

Expand Down Expand Up @@ -78,22 +46,7 @@ export const defineStepper = <const Steps extends Step[]>(...steps: Steps): Step
reset() {
setStepIndex(initialStepIndex);
},
switch(when) {
const whenFn = when[current.id as keyof typeof when];
return whenFn?.(current as Get.StepById<typeof steps, (typeof current)["id"]>);
},
when(id, whenFn, elseFn) {
const currentStep = steps[stepIndex];
const matchesId = Array.isArray(id)
? currentStep.id === id[0] && id.slice(1).every(Boolean)
: currentStep.id === id;

return matchesId ? whenFn?.(currentStep as any) : elseFn?.(currentStep as any);
},
match(state, matches) {
const matchFn = matches[state as keyof typeof matches];
return matchFn?.(state as any);
},
...generateCommonStepperUseFns(steps, current, stepIndex),
} as Stepper<Steps>;
}, [stepIndex]);

Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Types
export type { Step, Stepper, Get } from "./types";
export type { Step, Stepper, Get } from '@stepperize/core'

// Define Stepper
export * from "./define-stepper";
Loading

0 comments on commit f8943d8

Please sign in to comment.