[State Machine] Zag-js

import { createMachine } from "@zag-js/core";

type MachineState = {
  value: "idle" | "focused";
};

type MachineContext = {
  value: string[];
  focusedIndex: number;
  name?: string;
  readonly isCompleted: boolean;
  onCompleted?: (value: string[]) => void;
};

export type MachineOptions = {
  value?: string[];
  onCompleted?: (value: string[]) => void;
  numOfFields: number;
  name?: string;
};

function getNextValue(eventValue: string, currentValue: string) {
  let nextValue = eventValue;
  if (currentValue[0] === eventValue[0]) {
    // "2", "29" => "9"
    nextValue = eventValue[1];
  } else if (currentValue[0] === eventValue[1]) {
    // "2", "92" => "9"
    nextValue = eventValue[0];
  }

  // "2", "22" => "2"
  return nextValue;
}

export function machine(options: MachineOptions) {
  const { numOfFields, name, ...restOptions } = options;
  return createMachine<MachineContext, MachineState>(
    {
      id: "pin-input",
      initial: "idle",
      context: {
        name: name ?? "pin-input",
        value: Array.from<string>({ length: numOfFields }).fill(""),
        focusedIndex: -1,
        ...restOptions,
      },
      computed: {
        isCompleted(context) {
          return context.value.every((val) => val !== "");
        },
      },
      watch: {
        focusedIndex: ["executeFocus"],
        isCompleted: ["invokeOnCompleted"],
      },
      states: {
        idle: {
          on: {
            FOCUS: {
              target: "focused",
              actions: ["setFocusedIndex"],
            },
            LABEL_CLICK: {
              actions: ["focusFirstInput"],
            },
          },
        },
        focused: {
          on: {
            BLUR: {
              target: "idle",
              actions: ["clearFocusedIndex"],
            },
            INPUT: {
              actions: ["setFocusedValue", "focusNextInput"],
            },
            BACKSPACE: {
              actions: ["clearFocusedValue", "focusPreviousInput"],
            },
            PASTE: {
              actions: ["setPastedValue", "focusLastEmptyInput"],
            },
          },
        },
      },
    },
    {
      actions: {
        setFocusedIndex(context, event) {
          context.focusedIndex = event.index;
        },
        clearFocusedValue(context) {
          context.value[context.focusedIndex] = "";
        },
        clearFocusedIndex(context, event) {
          context.focusedIndex = -1;
        },
        setFocusedValue(context, event) {
          // should only allow latest input value
          const nextValue = getNextValue(
            event.value,
            context.value[context.focusedIndex]
          );
          context.value[context.focusedIndex] = nextValue;
        },
        setPastedValue(context, event) {
          const pastedValue: string[] = event.value
            .split("")
            .slice(0, context.value.length);
          pastedValue.forEach((value, index) => {
            context.value[index] = value;
          });
        },
        focusFirstInput(context) {
          context.focusedIndex = 0;
        },
        focusNextInput(context, event) {
          const nextIndex = Math.min(event.index + 1, context.value.length - 1);
          context.focusedIndex = nextIndex;
        },
        focusPreviousInput(context, event) {
          const prevIndex = Math.max(context.focusedIndex - 1, 0);
          context.focusedIndex = prevIndex;
        },
        focusLastEmptyInput(context) {
          const index = context.value.findIndex((value) => value === "");
          const lastIndex = context.value.length - 1;
          context.focusedIndex = index === -1 ? lastIndex : index;
        },
        executeFocus(context) {
          const inpugGroup = document.querySelector("[data-part=input-group]");
          if (!inpugGroup || context.focusedIndex < 0) return;
          const inputElements = Array.from<HTMLInputElement>(
            document.querySelectorAll("[data-part=input]")
          );
          const input = inputElements[context.focusedIndex];
          // avoid mutli dom mutation problem
          // schedule for next event
          requestAnimationFrame(() => {
            input?.focus();
          });
        },
        invokeOnCompleted(context) {
          if (!context.isCompleted) return;
          context.onCompleted?.(Array.from(context.value));
        },
      },
    }
  );
}

 

posted @ 2023-02-19 03:25  Zhentiw  阅读(24)  评论(0编辑  收藏  举报