npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

easy-tutorial-react

v0.3.3

Published

Write onboarding tutorial with a declarative API

Downloads

2

Readme

Why easy-tutorial-react

  1. Easy-to-use Declarative API
  2. Full control over rendering process
  3. Use any UI library you want to write your own render function
  4. Write code that is completely separate from the main rendering logic
  5. Default supports rendering in both light mode and dark mode
  6. Completely written in TypeScript
  7. MIT LICENSE

Installation

npm install easy-tutorial-react

# OR
yarn add easy-tutorial-react

# OR
pnpm add easy-tutorial-react

Quick Start

Create a EasyTutorial class and pass it as a dataSource to the EasyTutorialRenderer and EasyTutorialNoticeRenderer.

import {
  EasyTutorial,
  EasyTutorialRenderer,
  EasyTutorialNoticeRenderer,
} from "easy-tutorial-react";

const easyTutorial = new EasyTutorial();

const intro = easyTutorial.addTutorial("intro")
introduction.addStep({
   // You can also use XPath for search target element.
   // Plase take a look at `Use XPath` section.
   targetQuery: "#page-title",
   // content must be a single JSX.Element
   content: <div>Hello from easy-tutorial-react, you can click next button to go next.</div>
})
introduction.addStep({
   targetQuery: "#page-contents",
   contents: <div>Step 2 here.</div>,
   // You can pass a placement to control where the tutorial appears.
   // Default is "bottom-left"
   placement: "bottom-center",
})

export default function App() {
   return (
      <>
         <EasyTutorialRenderer dataSource={easyTutorial} />
         <EasyTutorialNoticeRenderer dataSource={easyTutorial} />

         <main>
            <button onClick={() => easyTutorial.start("intro")}>
               Start Tutorial
            </button>

            <h1 id="page-title">Page Title</h1>
            <p id="page-contents">Page contents</p>
         </main>
      </>
   )
}

Notice

We won't make any changes to the browser's DOM before you actively render the EasyTutorialRenderer.

So, We recommend having the EasyTutorialRenderer as one of the first components to render in your React project, such as placing it in App.tsx or another initial component where your React app renders.

Use XPath

You can use XPath in targetQuery for searching the target element.

This means you don't have to assign an ID to every target element in order to locate its position.

This gives easy-tutorial-react the ability to write code that is completely separated from the main rendering logic.

You can quickly find the elements you need to locate in a complex and multi-page system, just by using the browser's F12 (Developer Tools).

Something like:

myTutorial.addStep({
   targetQuery: `/html/body/ntp-app//div/div[2]/ntp-realbox//div/input`,
   content: <div>Hello tutorial.</div>
})

Notice

Some UI frameworks may change the structure of the DOM tree. ( Such as ChakraUI, which adds some extra nodes to the document when changing color mode. )

This can lead to the same XPath locating diffrent elements.

To solve this problem, we recommend still adding some IDs to your components. This IDs will act as some "anchors", helping XPath to precisely locate your elements.

For example, add a top-level ID to each page component:

const Header = () => {
   return (
      <div id="my-header">
         Header
      </div>
   )
}

const Footer = () => {
   return (
      <div id="my-footer">
         Footer
      </div>
   )
}

const UserPage = () => {
   return (
      <div id="user-page">
         User Page
      </div>
   )
}

export default function App() {
   return (
      <>
         <Header />

         <UserPage />

         <Footer />
      </>
   )
}

TypeScript Best Practices

First, in your App.tsx or another component where your React app first renders, add EasyTutorialRenderer and EasyTutorialNoticeRenderer components.

// App.tsx //

import {
  EasyTutorialRenderer,
  EasyTutorialNoticeRenderer,
} from "easy-tutorial-react";

export default function App() {
   // ...
   return (
      <>
         {/* If you are using typescript,
             you will get some type error here.
             Don't worry, we will fix it later. */}
         <EasyTutorialRenderer />
         <EasyTutorialNoticeRenderer />

         {/* Other components here */}
      </>
   )
}

Then, create a new file called tutorial.tsx, or any other name you prefer. We will write our tutorial code inside it.

// tutorial.tsx //

import { EasyTutorial } from "easy-tutorial-react";

/** Type EasyTutorial Class  */
type MyTutorials = "intro" | "how-to-use"

// Export easyTutorial, we will use it in `App.tsx`
export const easyTutorial = new EasyTutorial<MyTutorials>();

/**
 * Create your tutorials by use `addTutorial` method.
 * Because we passed a custom type to the EasyTutorial Class,
 * IDE will automatically suggest the tutorial names we can choose from.
 */
const intro = easyTutorial.addTutorial("intro")

// Then we can use `addStep` to add steps to the intro tutorial.
intro.addStep({
   // ...
})

// You can do the same things for other tutorials you have created.
const howToUse = easyTutorial.addTutorial("how-to-use")
howToUse.addStep({
   // ...
})

Finally, we need to pass easyTutorial as a dataSource to the renderer.

// App.tsx //

import {
  EasyTutorialRenderer,
  EasyTutorialNoticeRenderer,
} from "easy-tutorial-react";
import { easyTutorial } from "./tutorial.tsx"

export default function App() {
   // ...
   return (
      <>
         <EasyTutorialRenderer dataSource={easyTutorial} />
         <EasyTutorialNoticeRenderer dataSource={easyTutorial} />

         {/* Other components here */}
      </>
   )
}

Optionally, easy-tutorial-react supports rendering in both light mode and dark mode by default.

You can pass a ColorMode variable in the first parameter of renderer's extendRenderArgs to render different components in diffrent modes.

In the convention, the type of ColorMode must be type ColorMode = "light" | "dark";

If you are using ChakraUI, you can do it like this:

// App.tsx //

import {
  EasyTutorialRenderer,
  EasyTutorialNoticeRenderer,
} from "easy-tutorial-react";
import { useColorMode } from "@chakra-ui/react";

import { easyTutorial } from "./tutorial.tsx"

export default function App() {
   const { colorMode } = useColorMode();

   // ...

   return (
      <>
         <EasyTutorialRenderer
            dataSource={easyTutorial}
            extendRenderArgs={[colorMode]}
         />
         <EasyTutorialNoticeRenderer
            dataSource={easyTutorial}
            extendRenderArgs={[colorMode]}
         />

         {/* Other components */}
      </>
   )
}

Configuration

Conditional Rendering

The addStep method accepts parameters such as canRender and noticeMsg for conditional rendering.

This can be used for elements that require the user to click a button before they are rendered.

For example:

myTutorial.addStep({
   targetQuery: "#target-elem",
   content: <div>Hello, tutorial</div>,
   canRender: () => {
      if (!document.querySelector("#some-element")) {
         return false
      }
      return true
   },
   noticeMsg: "You need to follow the tutorial and click the button before go to the next step."
   noticeTitle: "Can't go next"
})

canRender is called every time an attempt is made to render that step, whether it's rendering the next step or back step.

noticeMsg is the message rendered when canRender for the next step returns false, while backNoticeMsg is for the back step.

This can be used for operations that cannot be repeatedly clicked within the same flow.

For example:

myTutorial.addStep({
   targetQuery: "#target-elem",
   content: <div>Hello, tutorial</div>,
   canRender: () => {
      if (/* some condition */) {
         return false
      }
      return true
   },
   backNoticeMsg: "You cannot do previous action twice."
   backNoticeTitle: "Cannot back"
})

The type definition for AddStepParams is as follows in this file:

type TutorialStep<A extends Array<any>> = {
  targetQuery: string;
  content: JSX.Element;
  render: RenderFunc<A>;
  noticeMsg: string;
  noticeTitle: string;
  backNoticeMsg: string;
  backNoticeTitle: string;
  noticeDuration: number;
  scrollInView: boolean;
  placement: Placement;
  canRender: () => boolean;
};
type AddStepParams<A extends Array<any>> = Partial<
  Omit<TutorialStep<A>, "targetQuery" | "content">
> & {
  targetQuery: string;
  content?: JSX.Element;
};

Use Custom Rendering

You have two ways to customize the rendering function.

One, you can call overrideDefaultRender method before addStep.

myTutorial.overrideDefaultRender((basicArgs, ...args) => {
  const { currentContent, next, prev, stop } = basicArgs;
  return (
   <div>
      {currentContent}
      <button onClick={prev}>Back</button>
      <button onClick={next}>Next</button>
      <button onClick={stop}>Close</button>
   </div>
  );
});

myTutorial.addStep({
   targetQuery: "#target-elem",
   content: <div>Hello tutorial</div>
})

Two, You can passing a render function in every addStep.

myTutorial.addStep({
   targetQuery: "#target-elem",
   render: (basicArgs, ...args) => {
      // ...
   }
})

The rendering function must accept a basicArgs, as well as any parameters passed through the renderer's extendRenderArgs.

The type definition for BasicArgs is as follows in this file:

type StepType = "last" | "first" | "common" | "single";
type Placement =
  | "top-left"
  | "top-right"
  | "top-center"
  | "bottom-left"
  | "bottom-right"
  | "bottom-center"
  | "left-top"
  | "left-bottom"
  | "left-center"
  | "right-top"
  | "right-bottom"
  | "right-center";
export type RenderFuncBasicArg = {
  targetElem: Element; // The target element found through targetQuery.
  stepType: StepType; // You should determine the rendering of buttons based on the stepType, for example, for 'single' you should not render `nextBtn` and `backBtn`.
  next: () => void; // Next button should call this function to go next.
  prev: () => void; // Back button should call this function to go back.
  stop: () => void; // Stop button should call this function to stop tutorial.
  placement: Placement; // The placement passed when calling addStep.
  totalStep: number;
  currentStep: number;
};
export type RenderFunc<A extends Array<any>> = (
  basicArg: RenderFuncBasicArg,
  ...args: A // The Args passed through the renderer's extendRenderArgs.
) => JSX.Element;

Use Custom Notice

easy-tutorial-react provides a default notice renderer called EasyTutorialNoticeRenderer, but you also have the option to implement your own notice renderer.

When rendering is not possible, the EasyTutorial Class will emit a "canNotRender" event, which you can listen to in order to provide feedback to the user.

The default notice render is written as a standalone React functional component for extensibility.

However, our own implementation logic doesn't necessarily need to be written as a separate component.

Once you have implemented your own notice render logic, you can delete the default EasyTutorialNoticeRenderer.

For example:

import { useEffect } from "react"
import {
  EasyTutorialRenderer,
} from "easy-tutorial-react";

const easyTutorial = new EasyTutorial();

export default function App() {
   // ...
   useEffect(() => {
      easyTutorial.on("canNotRender", ({ msg, title, duration }) => {
         window.alert(msg);
      });
   }, []);

   return (
      <>
         <EasyTutorialRenderer dataSource={easyTutorial} />

         {/* Other components */}
      </>
   )
}

The default implementation is in this file.

LICENSE

MIT