Skip to content

Commit

Permalink
docs(examples): add todomvc example
Browse files Browse the repository at this point in the history
  • Loading branch information
UberMouse committed Sep 26, 2022
1 parent 6e4b375 commit a638915
Show file tree
Hide file tree
Showing 16 changed files with 2,155 additions and 191 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/xstate-tree.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ jobs:
run: npm run lint
- name: Run Tests
run: npm run test
- name: Test examples
run: npm run test-examples
- name: Build
run: npm run build
- name: API Extractor
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
node_modules
lib
out
temp
245 changes: 245 additions & 0 deletions examples/todomvc/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
import {
broadcast,
buildActions,
buildSelectors,
buildView,
buildXStateTreeMachine,
multiSlot,
Link,
RoutingEvent,
} from "@koordinates/xstate-tree";
import { assign } from "@xstate/immer";
import React from "react";
import { map } from "rxjs/operators";
import { createMachine, type ActorRefFrom, spawn } from "xstate";

import { TodoMachine } from "./Todo";
import { todos$, type Todo } from "./models";
import { activeTodos, allTodos, completedTodos } from "./routes";

type Context = {
todos: Todo[];
actors: Record<string, ActorRefFrom<typeof TodoMachine>>;
filter: "all" | "active" | "completed";
};
type Events =
| { type: "SYNC_TODOS"; todos: Todo[] }
| RoutingEvent<typeof activeTodos>
| RoutingEvent<typeof allTodos>
| RoutingEvent<typeof completedTodos>;

const TodosSlot = multiSlot("Todos");
const slots = [TodosSlot];
const machine =
/** @xstate-layout N4IgpgJg5mDOIC5QBcD2FUFoCGAHXAdAMaoB2EArkWgE4DEiouqsAlsq2YyAB6ICMAFgBMBAJwTBANgkBWAOwAOWQGYpwgDQgAngMEEp-RYNn8FABmHmxMlQF87WtBhz46AZQCaAOQDCAfQAVAHkAEWD3bmY2Di4kXkRMeSkCEWTzRUUbYUV5c1ktXQRMflKCMyUxQXk1Y2FShyd0LDxcDwAJYIB1fwBBX0CASQA1AFEgsIiolnZOUm4+BBUagitzFWFZasVzczMpQsThFeVFFV35fjzLQUaQZxa3d06e3oAZN4nwyPjo2bjQIt+FJFKsxNYxLIbLJNoIxIpDsUVFDUsJBGcVGIrPxjrdHPdmq42s9uv5fMEALIABTeo0Co1CXymvxmsXm8UWJWsBB2ljUVThe2EBx0iWRYlR6JUmOxuIc+NI6Dg3AeROIZEo1FQNGmMTmC0SslBVRqIJE-HywsEgkRVwIlWqan40tM-DuqtaBEVgWa8BZeoBCQQV1EUhUFSyjrNNtFwZs4kjpysKkUwjU7sJnoAFthYD6MH6mKz9RzDeYCDCwxGTfzbfH4VUk+tU+n8R78Lr-uzAYkLeW0lIMll1Ll8ojMGiUhGzkIU2JWw4gA */
createMachine(
{
context: { todos: [], actors: {}, filter: "all" },
tsTypes: {} as import("./App.typegen").Typegen0,
schema: { context: {} as Context, events: {} as Events },
predictableActionArguments: true,
invoke: {
src: "syncTodos",
id: "syncTodos",
},
id: "todo-app",
initial: "conductor",
on: {
SYNC_TODOS: {
actions: "syncTodos",
target: ".conductor",
},
SHOW_ACTIVE_TODOS: {
actions: "setFilter",
},
SHOW_ALL_TODOS: {
actions: "setFilter",
},
SHOW_COMPLETED_TODOS: {
actions: "setFilter",
},
},
states: {
conductor: {
always: [
{
cond: "hasTodos",
target: "hasTodos",
},
{
target: "noTodos",
},
],
},
noTodos: {},
hasTodos: {},
},
},
{
actions: {
syncTodos: assign((ctx, e) => {
ctx.todos = e.todos;

ctx.todos.forEach((todo) => {
if (!ctx.actors[todo.id]) {
ctx.actors[todo.id] = spawn(
TodoMachine.withContext({ ...TodoMachine.context, todo }),
TodosSlot.getId(todo.id)
);
}
});
}),
setFilter: assign((ctx, e) => {
ctx.filter =
e.type === "SHOW_ACTIVE_TODOS"
? "active"
: e.type === "SHOW_COMPLETED_TODOS"
? "completed"
: "all";
}),
},
guards: {
hasTodos: (ctx) => ctx.todos.length > 0,
},
services: {
syncTodos: () => {
return todos$.pipe(
map((todos): Events => ({ type: "SYNC_TODOS", todos }))
);
},
},
}
);

const selectors = buildSelectors(machine, (ctx) => ({
get count() {
const completedCount = ctx.todos.filter((t) => t.completed).length;
const activeCount = ctx.todos.length - completedCount;

return ctx.filter === "completed" ? completedCount : activeCount;
},
get countText() {
const count = this.count;
const plural = count === 1 ? "" : "s";

return `item${plural} ${ctx.filter === "completed" ? "completed" : "left"}`;
},
allCompleted: ctx.todos.every((t) => t.completed),
haveCompleted: ctx.todos.some((t) => t.completed),
allTodosClass: ctx.filter === "all" ? "selected" : undefined,
activeTodosClass: ctx.filter === "active" ? "selected" : undefined,
completedTodosClass: ctx.filter === "completed" ? "selected" : undefined,
}));

const actions = buildActions(machine, selectors, () => ({
addTodo(title: string) {
const trimmed = title.trim();

if (trimmed.length > 0) {
broadcast({ type: "TODO_CREATED", text: trimmed });
}
},
completeAll(completed: boolean) {
broadcast({ type: "TODO_ALL_COMPLETED", completed });
},
clearCompleted() {
broadcast({ type: "TODO_COMPLETED_CLEARED" });
},
}));

const view = buildView(
machine,
selectors,
actions,
slots,
({ inState, actions, selectors, slots }) => {
return (
<>
<section className="todoapp">
<header className="header">
<h1>todos</h1>
<input
className="new-todo"
placeholder="What needs to be done?"
autoFocus
onKeyDown={(e) => {
if (e.key === "Enter") {
actions.addTodo(e.currentTarget.value);
}
}}
/>
</header>

{inState("hasTodos") && (
<>
<section className="main">
<input
id="toggle-all"
className="toggle-all"
type="checkbox"
onChange={() => actions.completeAll(!selectors.allCompleted)}
checked={selectors.allCompleted}
/>
<label htmlFor="toggle-all">Mark all as complete</label>

<ul className="todo-list">
<slots.Todos />
</ul>
</section>

<footer className="footer">
<span className="todo-count">
<strong>{selectors.count}</strong> {selectors.countText}
</span>
<ul className="filters">
<li>
<Link to={allTodos} className={selectors.allTodosClass}>
All
</Link>
</li>
<li>
<Link
to={activeTodos}
className={selectors.activeTodosClass}
>
Active
</Link>
</li>
<li>
<Link
to={completedTodos}
className={selectors.completedTodosClass}
>
Completed
</Link>
</li>
</ul>

{selectors.haveCompleted && (
<button
className="clear-completed"
onClick={actions.clearCompleted}
>
Clear completed
</button>
)}
</footer>
</>
)}
</section>

<footer className="info">
<p>Double-click to edit a todo</p>
<p>Created by Taylor Lodge</p>
</footer>
</>
);
}
);

export const TodoApp = buildXStateTreeMachine(machine, {
view,
actions,
selectors,
slots,
});
40 changes: 40 additions & 0 deletions examples/todomvc/App.typegen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// This file was automatically generated. Edits will be overwritten

export interface Typegen0 {
"@@xstate/typegen": true;
internalEvents: {
"": { type: "" };
"done.invoke.syncTodos": {
type: "done.invoke.syncTodos";
data: unknown;
__tip: "See the XState TS docs to learn how to strongly type this.";
};
"error.platform.syncTodos": {
type: "error.platform.syncTodos";
data: unknown;
};
"xstate.init": { type: "xstate.init" };
};
invokeSrcNameMap: {
syncTodos: "done.invoke.syncTodos";
};
missingImplementations: {
actions: never;
services: never;
guards: never;
delays: never;
};
eventsCausingActions: {
setFilter: "SHOW_ACTIVE_TODOS" | "SHOW_ALL_TODOS" | "SHOW_COMPLETED_TODOS";
syncTodos: "SYNC_TODOS";
};
eventsCausingServices: {
syncTodos: "xstate.init";
};
eventsCausingGuards: {
hasTodos: "";
};
eventsCausingDelays: {};
matchesStates: "conductor" | "hasTodos" | "noTodos";
tags: never;
}
Loading

0 comments on commit a638915

Please sign in to comment.