Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions templates/lit-ts/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
lib
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
9 changes: 9 additions & 0 deletions templates/lit-ts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# XState Lit TypeScript template

A starting point template for using XState with [Lit](https://lit.dev) and TypeScript. Create a feedback form using a simple state machine.

Using [Vite](https://vitejs.dev/) as a build tool and to run the local development server.

## [➡️ Open in CodeSandbox](https://codesandbox.io/p/sandbox/github/statelyai/xstate/tree/main/templates/lit-ts?file=%2Fsrc%2FfeedbackMachine.ts)

[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/statelyai/xstate/tree/main/templates/lit-ts?file=%2Fsrc%2FfeedbackMachine.ts)
33 changes: 33 additions & 0 deletions templates/lit-ts/demo/counter.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<!doctype html>
<html lang="en">
<head>
<title>Demo - feedback-element</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="data:image/x-icon;base64,B" />
<meta name="description" content="feedback-element" />
<style>
html,
body {
height: 100%;
margin: 0;
font-family: sans-serif;
}
body {
display: flex;
justify-content: center;
align-items: center;
background: #eaeaea;
}
:not(:defined) {
visibility: hidden;
}
</style>
</head>
<body>
<counter-element>
<a href="./index.html">View feedback demo</a>
</counter-element>
<script src="./entry.js" type="module"></script>
</body>
</html>
2 changes: 2 additions & 0 deletions templates/lit-ts/demo/entry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import '../src/define/feedback-element.ts';
import '../src/define/counter-element.ts';
33 changes: 33 additions & 0 deletions templates/lit-ts/demo/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<!doctype html>
<html lang="en">
<head>
<title>Demo - feedback-element</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="data:image/x-icon;base64,B" />
<meta name="description" content="feedback-element" />
<style>
html,
body {
height: 100%;
margin: 0;
font-family: sans-serif;
}
body {
display: flex;
justify-content: center;
align-items: center;
background: #eaeaea;
}
:not(:defined) {
visibility: hidden;
}
</style>
</head>
<body>
<feedback-element>
<a href="./counter.html">View counter demo</a>
</feedback-element>
<script src="./entry.js" type="module"></script>
</body>
</html>
10 changes: 10 additions & 0 deletions templates/lit-ts/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Redirect feedback-element url=/demo/index.html"</title>
<meta http-equiv="refresh" content="0; url=/demo/index.html" />
</head>
<body></body>
</html>
27 changes: 27 additions & 0 deletions templates/lit-ts/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "lit-ts",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build && npm run build:lib",
"build:lib": "vite build --config vite.lib.config.js && tsc",
"preview": "vite preview"
},
"devDependencies": {
"@custom-elements-manifest/analyzer": "^0.10.4",
"@web/rollup-plugin-html": "^2.3.0",
"rollup-plugin-copy": "^3.5.0",
"rollup-plugin-node-externals": "^8.0.1",
"tinyglobby": "^0.2.14",
"tslib": "^2.8.1",
"typescript": "^5.8.3",
"vite": "^7.0.6"
},
"dependencies": {
"@statelyai/inspect": "^0.4.0",
"lit": "^3.3.1",
"xstate": "^5.20.1"
}
}
5 changes: 5 additions & 0 deletions templates/lit-ts/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

98 changes: 98 additions & 0 deletions templates/lit-ts/src/CounterElement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { html, LitElement } from 'lit';
import { state } from 'lit/decorators.js';
import { type InspectionEvent, type SnapshotFrom } from 'xstate';
import { counterMachine } from './counterMachine.js';
import { UseMachine } from '@xstate/lit';
import { styles } from './styles/counter-element-styles.css.js';

export class CounterElement extends LitElement {
static override styles = [styles];

#inspectEventsHandler: (inspEvent: InspectionEvent) => void =
this.#inspectEvents.bind(this);

#callbackHandler: (snapshot: SnapshotFrom<any>) => void =
this.#callbackCounterController.bind(this);

counterController: UseMachine<typeof counterMachine> = new UseMachine(this, {
machine: counterMachine,
options: {
inspect: this.#inspectEventsHandler
},
callback: this.#callbackHandler
});

@state()
xstate: typeof this.counterController.snapshot =
this.counterController.snapshot;

override updated(props: Map<string, unknown>) {
super.updated && super.updated(props);
if (props.has('xstate')) {
const { context, value } = this.xstate;
const detail = { ...(context || {}), value };
const counterEvent = new CustomEvent('counterchange', {
bubbles: true,
detail
});
this.dispatchEvent(counterEvent);
}
}

#callbackCounterController(snapshot: typeof this.counterController.snapshot) {
this.xstate = snapshot;
}

#inspectEvents(inspEvent: InspectionEvent) {
if (
inspEvent.type === '@xstate.snapshot' &&
inspEvent.event.type === 'xstate.stop'
) {
this.xstate = {} as unknown as typeof this.counterController.snapshot;
}
}

get #disabled() {
return this.counterController.snapshot.matches('disabled');
}

#send(event: any) {
this.counterController.send(event);
}

override render() {
return html`
<div aria-disabled="${this.#disabled}">
<span>
<button
?disabled="${this.#disabled}"
data-counter="increment"
@click=${() => this.#send({ type: 'INC' })}
>
Increment
</button>
<button
?disabled="${this.#disabled}"
data-counter="decrement"
@click=${() => this.#send({ type: 'DEC' })}
>
Decrement
</button>
</span>
<p>${this.counterController.snapshot.context.counter}</p>
</div>
<div>
<button @click=${() => this.#send({ type: 'TOGGLE' })}>
${this.#disabled ? 'Enabled counter' : 'Disabled counter'}
</button>
<span><slot></slot></span>
</div>
`;
}
}

declare global {
interface HTMLElementTagNameMap {
'counter-element': CounterElement;
}
}
Loading