Skip to content

Commit 545c4b5

Browse files
authored
Add recipe for Zag
1 parent 906c890 commit 545c4b5

File tree

9 files changed

+344
-2
lines changed

9 files changed

+344
-2
lines changed

Diff for: website/src/content/docs/recipes/zag.mdx

+120
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
---
2+
title: "Zag"
3+
---
4+
5+
import { Tabs, TabItem } from "@astrojs/starlight/components";
6+
import CodeEditor from "~/components/CodeEditor.astro";
7+
import { zag } from "~/source-examples/sources";
8+
9+
## Background
10+
11+
[Zag](https://zagjs.com/) is a new approach to the component design process, designed to help you avoid re-inventing the wheel and build better UI components regardless of framework.
12+
Zag includes a number of packages:
13+
14+
* A core state library based on state machines, inspired by and similar to [XState](/recipes/xstate/).
15+
* Framework adapters, to use that state inside of React, Vue, Solid, Svelte and others.
16+
* Out-of-the box machines for common component patterns.
17+
18+
Zag is defined by default for component-level state, but provides APIs like `useActor`, `useService`, `useSnapshot` and `useMachine`.
19+
20+
Bunshi helps make Zag machines easier to share by creating, starting and stopping a Zag service at just the right times.
21+
22+
## Component state
23+
24+
Bunshi lets you create Zag machines per component. This is the scope that state is normally defined in Zag by using the provided `useMachine` API. Using Bunshi we can define state
25+
at the same scope. We define a component-scoped molecule, and [lifecycle hooks](/concepts/lifecycle/) to start it and stop it at the right times.
26+
27+
<Tabs>
28+
<TabItem label="React">
29+
<CodeEditor
30+
files={{
31+
"App.tsx": zag.ReactApp,
32+
"style.css": zag.style,
33+
"molecules.ts": {
34+
code: zag.molecules2,
35+
active: true,
36+
},
37+
}}
38+
dependencies={{
39+
"@zag-js/pagination": "^0.40",
40+
"@zag-js/react": "^0.40",
41+
}}
42+
/>
43+
</TabItem>
44+
<TabItem label="Vue">
45+
<CodeEditor
46+
files={{
47+
"src/App.vue": zag.VueApp,
48+
"src/Pagination.vue": zag.VuePagination,
49+
"src/style.css": zag.style,
50+
"src/molecules.ts": {
51+
code: zag.molecules2,
52+
active: true,
53+
},
54+
}}
55+
dependencies={{
56+
"@zag-js/pagination": "^0.40",
57+
"@zag-js/vue": "^0.40",
58+
}}
59+
template="vue"
60+
/>
61+
</TabItem>
62+
</Tabs>
63+
64+
## Global state
65+
66+
If you want to share some state between components in Zag, then things become more tricky using the provided APIs. You can use `useService` to
67+
create a service, store in framework state, then share it `provide/inject` in vue or `Context` in React, and then use `useActor` in children components.
68+
This is time-consuming boilerplate just to be able to share state.
69+
70+
**This is where Bunshi comes in.** You can use Zag to define a machine that is shared across your application, and Bunshi will look after creating it, starting it
71+
and stopping it at the right times.
72+
73+
<Tabs>
74+
<TabItem label="React">
75+
<CodeEditor
76+
files={{
77+
"App.tsx": zag.ReactApp,
78+
"style.css": zag.style,
79+
"molecules.ts": {
80+
code: zag.molecules,
81+
active: true,
82+
},
83+
}}
84+
dependencies={{
85+
"@zag-js/pagination": "^0.40",
86+
"@zag-js/react": "^0.40",
87+
}}
88+
/>
89+
</TabItem>
90+
<TabItem label="Vue">
91+
<CodeEditor
92+
files={{
93+
"src/App.vue": zag.VueApp,
94+
"src/Pagination.vue": zag.VuePagination,
95+
"src/style.css": zag.style,
96+
"src/molecules.ts": {
97+
code: zag.molecules,
98+
active: true,
99+
},
100+
}}
101+
dependencies={{
102+
"@zag-js/pagination": "^0.40",
103+
"@zag-js/vue": "^0.40",
104+
}}
105+
template="vue"
106+
/>
107+
</TabItem>
108+
</Tabs>
109+
110+
## Why Bunshi with Zag?
111+
112+
Bunshi helps scope your Zag machines so they are easy to share. Notice how in this example you didn't need to touch
113+
your Pagination component to move the state to being global or component scoped. Bunshi helps remove
114+
boilerplate and centralize your state management tools.
115+
116+
* Use Zag for global state stores that are started and stopped at the exactly right time.
117+
* Move state without touching components
118+
* Use the vanilla javascript API of Zag and avoid framework lock-in
119+
* Share machines across different pages, components or sections
120+
* Keep your state logic decoupled from your UI framework

Diff for: website/src/source-examples/sources.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
export { default as jotai } from "./jotai/sources";
2+
export { default as lifecycle } from "./lifecycle/sources";
23
export { default as nanostores } from "./nanostores/sources";
34
export { default as quickstart } from "./quickstart/sources";
45
export { default as valtio } from "./valtio/sources";
6+
export { default as vueRefs } from "./vue-refs/sources";
57
export { default as xstate } from "./xstate/sources";
8+
export { default as zag } from "./zag/sources";
69
export { default as zustand } from "./zustand/sources";
7-
export { default as vueRefs } from "./vue-refs/sources";
8-
export { default as lifecycle } from "./lifecycle/sources";
10+

Diff for: website/src/source-examples/zag/App.tsx

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { useMolecule } from "bunshi/react";
2+
import React from "react";
3+
4+
import * as pagination from "@zag-js/pagination";
5+
import { normalizeProps, useActor } from "@zag-js/react";
6+
7+
import { PaginationMolecule } from "./molecules";
8+
import "./style.css";
9+
10+
function Pagination() {
11+
const actor = useMolecule(PaginationMolecule);
12+
const [state, send] = useActor(actor);
13+
14+
const api = pagination.connect(state, send, normalizeProps)
15+
16+
return (
17+
<div>
18+
{api.totalPages > 1 && (
19+
<nav {...api.rootProps}>
20+
<ul>
21+
<li>
22+
<a href="#previous" {...api.prevTriggerProps}>
23+
&lt; <span className="visually-hidden">Previous Page</span>
24+
</a>
25+
</li>
26+
{api.pages.map((page, i) => {
27+
if (page.type === "page")
28+
return (
29+
<li key={page.value}>
30+
<a href={`#${page.value}`} {...api.getItemProps(page)}>
31+
{page.value}
32+
</a>
33+
</li>
34+
)
35+
else
36+
return (
37+
<li key={`ellipsis-${i}`}>
38+
<span {...api.getEllipsisProps({ index: i })}>&#8230;</span>
39+
</li>
40+
)
41+
})}
42+
<li>
43+
<a href="#next" {...api.nextTriggerProps}>
44+
&gt; <span className="visually-hidden">Next Page</span>
45+
</a>
46+
</li>
47+
</ul>
48+
</nav>
49+
)}
50+
</div>
51+
)
52+
}
53+
export default function App() {
54+
return (
55+
<div>
56+
<Pagination />
57+
<Pagination />
58+
</div>
59+
);
60+
}

Diff for: website/src/source-examples/zag/App.vue

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script setup>
2+
import Pagination from "./Pagination.vue";
3+
</script>
4+
<template>
5+
<Pagination />
6+
<Pagination />
7+
</template>

Diff for: website/src/source-examples/zag/Pagination.vue

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<script setup>
2+
import { useMolecule } from "bunshi/vue";
3+
4+
import * as pagination from "@zag-js/pagination";
5+
import { normalizeProps, useActor } from "@zag-js/vue";
6+
import { computed } from "vue";
7+
8+
import { PaginationMolecule } from "./molecules";
9+
import "./style.css";
10+
11+
const actor = useMolecule(PaginationMolecule);
12+
const [state, send] = useActor(actor);
13+
14+
const api = computed(() => pagination.connect(state.value, send, normalizeProps))
15+
</script>
16+
17+
<template>
18+
<nav v-if="api.totalPages > 1" v-bind="api.rootProps">
19+
<ul>
20+
<li>
21+
<a href="#previous" v-bind="api.prevTriggerProps">
22+
&lt; <span class="visually-hidden">Previous Page</span>
23+
</a>
24+
</li>
25+
<li
26+
v-for="(page, i) in api.pages"
27+
:key="page.type === 'page' ? page.value : `ellipsis-${i}`"
28+
>
29+
<span v-if="page.type === 'page'">
30+
<a :href="`#${page.value}`" v-bind="api.getItemProps(page)">
31+
{{page.value}}
32+
</a></span
33+
>
34+
<span v-else>
35+
<span v-bind="api.getEllipsisProps({ index: i })">&#8230;</span>
36+
</span>
37+
</li>
38+
<li>
39+
<a href="#next" v-bind="api.nextTriggerProps">
40+
&gt; <span class="visually-hidden">Next Page</span>
41+
</a>
42+
</li>
43+
</ul>
44+
</nav>
45+
</template>

Diff for: website/src/source-examples/zag/molecules.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { molecule, onMount } from "bunshi";
2+
import * as pagination from "@zag-js/pagination"
3+
4+
5+
export const PaginationMolecule = molecule(() => {
6+
7+
const service = pagination.machine({ count: 100, pageSize: 20 });
8+
9+
onMount(() => {
10+
service._created();
11+
return () => service.stop();
12+
});
13+
return service;
14+
});

Diff for: website/src/source-examples/zag/molecules2.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { ComponentScope, molecule, onMount, use } from "bunshi";
2+
import * as pagination from "@zag-js/pagination"
3+
4+
export const PaginationMolecule = molecule(() => {
5+
use(ComponentScope)
6+
7+
const service = pagination.machine({ count: 100, pageSize: 20 });
8+
9+
onMount(() => {
10+
service._created();
11+
return () => service.stop();
12+
});
13+
return service;
14+
});

Diff for: website/src/source-examples/zag/sources.ts

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import ReactApp from "./App.tsx?raw";
2+
import VueApp from "./App.vue?raw";
3+
import VuePagination from "./Pagination.vue?raw";
4+
import molecules from "./molecules.ts?raw";
5+
import molecules2 from "./molecules2.ts?raw";
6+
import style from "./style.css?raw"
7+
8+
const zag = {
9+
molecules,
10+
molecules2,
11+
VueApp,
12+
VuePagination,
13+
ReactApp,
14+
style
15+
};
16+
17+
export default zag;

Diff for: website/src/source-examples/zag/style.css

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
.visually-hidden {
2+
display: none;
3+
}
4+
5+
[data-part="root"] {
6+
/* styles for the pagination's root */
7+
}
8+
9+
[data-part="root"] ul {
10+
display: flex;
11+
flex-wrap: wrap;
12+
gap: 0.25rem;
13+
list-style-type: none;
14+
}
15+
16+
[data-part="item"] {
17+
/* styles for the pagination's items */
18+
padding-inline: 0.75rem;
19+
height: 2rem;
20+
text-align: center;
21+
margin: auto 4px;
22+
color: #999;
23+
display: flex;
24+
align-items: center;
25+
letter-spacing: 0.01071em;
26+
line-height: 1.43;
27+
font-size: 13px;
28+
user-select: none;
29+
text-decoration: none;
30+
border: 1px solid;
31+
border-color: #ccc;
32+
background: #fff;
33+
cursor: pointer;
34+
}
35+
36+
[data-part="ellipsis"] {
37+
/* styles for the pagination's ellipsis */
38+
}
39+
40+
[data-part="prev-trigger"] {
41+
/* styles for the pagination's previous page trigger */
42+
}
43+
44+
[data-part="next-trigger"] {
45+
/* styles for the pagination's next page trigger */
46+
}
47+
48+
/* We add a data-disabled attribute to the prev/next items when on the first/last page */
49+
50+
[data-part="prev-trigger"][data-disabled] {
51+
/* styles for the pagination's previous page trigger when on first page */
52+
}
53+
54+
[data-part="next-trigger"][data-disabled] {
55+
/* styles for the pagination's next page trigger when on first page */
56+
}
57+
58+
[data-selected] {
59+
font-weight: bold;
60+
text-decoration: none;
61+
color: black;
62+
background: rgb(56, 161, 105);
63+
}

0 commit comments

Comments
 (0)