From eb71907368defbdc780b1b8fe70f102bae53da76 Mon Sep 17 00:00:00 2001 From: Segun Adebayo Date: Fri, 24 May 2024 17:31:05 +0100 Subject: [PATCH] refactor: vanilla example --- examples/vanilla-ts/index.html | 28 ++++--- examples/vanilla-ts/package.json | 4 - examples/vanilla-ts/src/accordion.ts | 75 ++++++++++++++++++ examples/vanilla-ts/src/main.ts | 90 ++-------------------- examples/vanilla-ts/src/normalize-props.ts | 46 +++++++++++ examples/vanilla-ts/src/spread-props.ts | 47 +++++++++++ pnpm-lock.yaml | 31 -------- 7 files changed, 193 insertions(+), 128 deletions(-) create mode 100644 examples/vanilla-ts/src/accordion.ts create mode 100644 examples/vanilla-ts/src/normalize-props.ts create mode 100644 examples/vanilla-ts/src/spread-props.ts diff --git a/examples/vanilla-ts/index.html b/examples/vanilla-ts/index.html index bddb402368..c629ba4b05 100644 --- a/examples/vanilla-ts/index.html +++ b/examples/vanilla-ts/index.html @@ -6,16 +6,26 @@ Vite + TS - -

Welcome to home

- + +

Accordion

+ +
+
+ +
First Content
+
+ +
+ +
Second Content
+
+ +
+ +
Third Content
+
+
- - diff --git a/examples/vanilla-ts/package.json b/examples/vanilla-ts/package.json index b916a9920d..9f8d06a0e5 100644 --- a/examples/vanilla-ts/package.json +++ b/examples/vanilla-ts/package.json @@ -77,13 +77,9 @@ "@zag-js/tree-view": "workspace:*", "@zag-js/types": "workspace:*", "@zag-js/utils": "workspace:*", - "alpinejs": "3.13.10", "form-serialize": "0.7.2", "match-sorter": "6.3.4", "typescript": "^5.2.2", "vite": "^5.2.0" - }, - "dependencies": { - "@types/alpinejs": "^3.13.10" } } diff --git a/examples/vanilla-ts/src/accordion.ts b/examples/vanilla-ts/src/accordion.ts new file mode 100644 index 0000000000..4e3d52f720 --- /dev/null +++ b/examples/vanilla-ts/src/accordion.ts @@ -0,0 +1,75 @@ +import * as accordion from "@zag-js/accordion" +import { normalizeProps } from "./normalize-props" +import { spreadProps } from "./spread-props" + +export class Accordion { + rootEl: HTMLElement + service: ReturnType + api: accordion.Api + + constructor(root: string, context: accordion.Context) { + const rootEl = document.querySelector(root) + + if (!rootEl) throw new Error("Root element not found") + this.rootEl = rootEl + + this.service = accordion.machine(context) + this.api = accordion.connect(this.service.state, this.service.send, normalizeProps) + } + + private disposable = new Map() + + init = () => { + const { service } = this + this.service.subscribe(() => { + this.api = accordion.connect(service.state, service.send, normalizeProps) + this.render() + }) + + this.service.start() + } + + destroy = () => { + this.service.stop() + } + + private get items() { + return Array.from(this.rootEl!.querySelectorAll(".accordion-item")) + } + + private renderRoot = () => { + const rootEl = this.rootEl + this.disposable.set(rootEl, spreadProps(this.rootEl, this.api.rootProps)) + } + + private renderItem = (itemEl: HTMLElement) => { + const value = itemEl.dataset.value + if (!value) throw new Error("Expected value to be defined") + + const itemTriggerEl = itemEl.querySelector(".accordion-trigger") + const itemContentEl = itemEl.querySelector(".accordion-content") + + if (!itemTriggerEl) throw new Error("Expected triggerEl to be defined") + if (!itemContentEl) throw new Error("Expected contentEl to be defined") + + const cleanup = this.disposable.get(itemEl) + cleanup?.() + + const cleanups = [ + spreadProps(itemEl, this.api.getItemProps({ value })), + spreadProps(itemTriggerEl, this.api.getItemTriggerProps({ value })), + spreadProps(itemContentEl, this.api.getItemContentProps({ value })), + ] + + this.disposable.set(itemEl, () => { + cleanups.forEach((fn) => fn()) + }) + } + + render = () => { + this.renderRoot() + this.items.forEach((itemEl) => { + this.renderItem(itemEl) + }) + } +} diff --git a/examples/vanilla-ts/src/main.ts b/examples/vanilla-ts/src/main.ts index 3e57bf49d6..f4718dcd64 100644 --- a/examples/vanilla-ts/src/main.ts +++ b/examples/vanilla-ts/src/main.ts @@ -1,87 +1,9 @@ -import * as checkbox from "@zag-js/checkbox" -import { subscribe } from "@zag-js/core" -import { createNormalizer } from "@zag-js/types" -import Alpine from "alpinejs" -import "../../../shared/src/style.css" +import { Accordion } from "./accordion" +import { nanoid } from "nanoid" -const propMap: any = { - htmlFor: "for", - className: "class", - onDoubleClick: "onDblclick", - onChange: "onInput", - onFocus: "onFocusin", - onBlur: "onFocusout", - defaultValue: "value", - defaultChecked: "checked", -} - -const toStyleString = (style: any) => { - let string = "" - for (let key in style) { - const value = style[key] - if (value === null || value === undefined) continue - if (!key.startsWith("--")) key = key.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`) - string += `${key}:${value};` - } - return string -} - -// all event handlers should use the format @click -const normalizeProps = createNormalizer((props: any) => { - return Object.entries(props).reduce((acc, [key, value]) => { - if (value === undefined) return acc - - if (key in propMap) { - key = propMap[key] - } - - if (key === "style" && typeof value === "object") { - acc.style = toStyleString(value) - return acc - } - - if (key.startsWith("on")) { - const _key = key.replace(/^on/, "@").toLowerCase() - acc[_key] = value - } else { - acc[`:${key.toLowerCase()}`] = () => value - } - - return acc - }, {}) -}) - -document.addEventListener("alpine:init", () => { - Alpine.magic("checkbox", (el, { Alpine, effect }) => (context: any) => { - const service = checkbox.machine(context) - - const state = Alpine.reactive({ value: service.getState() }) - - service.start() - - effect(() => { - service.setContext(context) - - const unsubscribe = subscribe(service.state, () => { - state.value = service.getState() - }) - - return () => { - unsubscribe() - service.stop() - } - }) - - Alpine.bind(el, { - "x-data"() { - return { - get api() { - return checkbox.connect(state.value, service.send, normalizeProps) - }, - } - }, - }) - }) +const accordion = new Accordion(".accordion", { + id: nanoid(), + multiple: true, }) -Alpine.start() +accordion.init() diff --git a/examples/vanilla-ts/src/normalize-props.ts b/examples/vanilla-ts/src/normalize-props.ts new file mode 100644 index 0000000000..5dfb4eb2b7 --- /dev/null +++ b/examples/vanilla-ts/src/normalize-props.ts @@ -0,0 +1,46 @@ +import { createNormalizer } from "@zag-js/types" + +export interface AttrMap { + [key: string]: string +} + +export const propMap: AttrMap = { + onFocus: "onFocusin", + onBlur: "onFocusout", + onChange: "onInput", + onDoubleClick: "onDblclick", + htmlFor: "for", + className: "class", + defaultValue: "value", + defaultChecked: "checked", +} + +const toStyleString = (style: any) => { + let string = "" + for (let key in style) { + const value = style[key] + if (value === null || value === undefined) continue + if (!key.startsWith("--")) key = key.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`) + string += `${key}:${value};` + } + return string +} + +export const normalizeProps = createNormalizer((props: any) => { + return Object.entries(props).reduce((acc, [key, value]) => { + if (value === undefined) return acc + + if (key in propMap) { + key = propMap[key] + } + + if (key === "style" && typeof value === "object") { + acc.style = toStyleString(value) + return acc + } + + acc[key.toLowerCase()] = value + + return acc + }, {}) +}) diff --git a/examples/vanilla-ts/src/spread-props.ts b/examples/vanilla-ts/src/spread-props.ts new file mode 100644 index 0000000000..0023d60e63 --- /dev/null +++ b/examples/vanilla-ts/src/spread-props.ts @@ -0,0 +1,47 @@ +export interface Attrs { + [key: string]: any // Change 'any' to the specific type you want to allow for attributes +} + +export function spreadProps(node: HTMLElement, attrs: Attrs): () => void { + const attrKeys = Object.keys(attrs) + + const addEvt = (e: string, f: EventListener) => { + node.addEventListener(e.toLowerCase(), f) + } + + const remEvt = (e: string, f: EventListener) => { + node.removeEventListener(e.toLowerCase(), f) + } + + const onEvents = (attr: string) => attr.startsWith("on") + const others = (attr: string) => !attr.startsWith("on") + + const setup = (attr: string) => addEvt(attr.substring(2), attrs[attr]) + const teardown = (attr: string) => remEvt(attr.substring(2), attrs[attr]) + + const apply = (attrName: string) => { + let value = attrs[attrName] + + if (typeof value === "boolean") { + value = value || undefined + } + + if (value != null) { + if (["value", "checked", "htmlFor"].includes(attrName)) { + ;(node as any)[attrName] = value // Using 'any' here because TypeScript can't narrow the type based on the array check + } else { + node.setAttribute(attrName.toLowerCase(), value) + } + return + } + + node.removeAttribute(attrName.toLowerCase()) + } + + attrKeys.filter(onEvents).forEach(setup) + attrKeys.filter(others).forEach(apply) + + return function cleanup() { + attrKeys.filter(onEvents).forEach(teardown) + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7123272d1c..7e1c0c05e3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1369,10 +1369,6 @@ importers: version: 4.3.2(typescript@5.4.5)(vite@5.2.11(@types/node@20.12.11)(terser@5.28.1)) examples/vanilla-ts: - dependencies: - '@types/alpinejs': - specifier: ^3.13.10 - version: 3.13.10 devDependencies: '@internationalized/date': specifier: 3.5.3 @@ -1578,9 +1574,6 @@ importers: '@zag-js/utils': specifier: workspace:* version: link:../../packages/utilities/core - alpinejs: - specifier: 3.13.10 - version: 3.13.10 form-serialize: specifier: 0.7.2 version: 0.7.2 @@ -6253,9 +6246,6 @@ packages: '@types/acorn@4.0.6': resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==} - '@types/alpinejs@3.13.10': - resolution: {integrity: sha512-ah53tF6mWuuwerpDE7EHwbZErNDJQlsLISPqJhYj2RZ9nuTYbRknSkqebUd3igkhLIZKkPa7IiXjSn9qsU9O2w==} - '@types/argparse@1.0.38': resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} @@ -6804,9 +6794,6 @@ packages: typescript: optional: true - '@vue/reactivity@3.1.5': - resolution: {integrity: sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg==} - '@vue/reactivity@3.4.27': resolution: {integrity: sha512-kK0g4NknW6JX2yySLpsm2jlunZJl2/RJGZ0H9ddHdfBVHcNzxmQ0sS0b09ipmBoQpY8JM2KmUw+a6sO8Zo+zIA==} @@ -6821,9 +6808,6 @@ packages: peerDependencies: vue: 3.4.27 - '@vue/shared@3.1.5': - resolution: {integrity: sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==} - '@vue/shared@3.4.21': resolution: {integrity: sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g==} @@ -6952,9 +6936,6 @@ packages: engines: {node: '>=4'} hasBin: true - alpinejs@3.13.10: - resolution: {integrity: sha512-86RB307VWICex0vG15Eq0x058cNNsvS57ohrjN6n/TJAVSFV+zXOK/E34nNHDHc6Poq+yTNCLqEzPqEkRBTMRQ==} - ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -15717,8 +15698,6 @@ snapshots: dependencies: '@types/estree': 1.0.5 - '@types/alpinejs@3.13.10': {} - '@types/argparse@1.0.38': {} '@types/babel__core@7.20.5': @@ -16573,10 +16552,6 @@ snapshots: optionalDependencies: typescript: 5.4.5 - '@vue/reactivity@3.1.5': - dependencies: - '@vue/shared': 3.1.5 - '@vue/reactivity@3.4.27': dependencies: '@vue/shared': 3.4.27 @@ -16598,8 +16573,6 @@ snapshots: '@vue/shared': 3.4.27 vue: 3.4.27(typescript@5.4.5) - '@vue/shared@3.1.5': {} - '@vue/shared@3.4.21': {} '@vue/shared@3.4.27': {} @@ -16728,10 +16701,6 @@ snapshots: transitivePeerDependencies: - encoding - alpinejs@3.13.10: - dependencies: - '@vue/reactivity': 3.1.5 - ansi-colors@4.1.3: {} ansi-escapes@4.3.2: