BlockquoteControllerXstate
连接XState机与Lit
blockquoteconcontrollerxstate是一个Lit Reactive Controller,专门设计用于与XState集成。该控制器提供订阅XState参与者的功能。它还调用 onSnapshot 随着状态的变化。
演示
使用
counterMachine.ts
import {setup, types} from 'xstate';
const counterSetup = setup({
// v6: `schemas` (Standard Schema). `types<T>()` is type-only, no runtime validation.
// Zod, Valibot, ArkType... can be used instead to validate at runtime.
schemas: {
context: types<{counter: number}>(),
events: {
INC: types<void>(),
DEC: types<void>(),
TOGGLE: types<void>(),
},
// The element's public events, dispatched on the host with `dispatchEmitted`
emitted: {
counterchange: types<{counter: number}>(),
},
},
guards: {
canIncrement: ({context}) => context.counter < 10,
canDecrement: ({context}) => context.counter > 0,
},
delays: {
backoff: ({context}) => context.counter * 1000,
},
});
export const counterMachine = counterSetup.createMachine({
context: {counter: 0},
initial: 'enabled',
states: {
enabled: {
on: {
// Transition functions (typed from `schemas`): return the next `context`, or
// `undefined` to ignore the event. The guard comes first: a function that calls
// `enq` handles the event even when it returns nothing.
INC: ({context, guards}, enq) => {
if (!guards.canIncrement({context})) {
return undefined;
}
const counter = context.counter + 1;
enq.emit({type: 'counterchange', counter});
return {context: {counter}};
},
DEC: ({context, guards}, enq) => {
if (!guards.canDecrement({context})) {
return undefined;
}
const counter = context.counter - 1;
enq.emit({type: 'counterchange', counter});
return {context: {counter}};
},
// v6 types do not accept the string shorthand (`TOGGLE: 'disabled'`)
TOGGLE: {target: 'disabled'},
},
},
disabled: {
after: {
backoff: {target: 'enabled'},
},
on: {
TOGGLE: {target: 'enabled'},
},
},
},
});
new BlockquoteControllerXstate(this, {machine, options, onSnapshot, onError, reconnect, dispatchEmitted})
this:主机组件(a)ReactiveControllerHost,通常是LitElement本身)。machine(必选):XState机。options:createActor选项(input,inspect,……)。可选,除非机器需要 输入:那{input}或者{snapshot}必须提供,当呼唤createActor.onSnapshot(可选):每次新快照时调用:每次连接的第一个快照 最后一个(snapshot.status === 'stopped'),此时主机已断开连接。onError(可选):当参与者出错时调用'error'快照也被发送到onSnapshot). 如果省略,XState将错误报告为未处理,除非另一个订阅者 演员处理它。reconnect(可选):'restart'(默认)或'restore'新演员的出发点是什么 当主机重新连接时。dispatchEmitted(可选):调度机器作为DOM事件在 主人。true使用{ composed: true }.
需要输入的机器
const userMachine = setup({
schemas: {
input: types<{userId: string}>(),
context: types<{userId: string}>(),
},
}).createMachine({
context: ({input}) => ({userId: input.userId}),
});
new BlockquoteControllerXstate(this, {machine: userMachine}); // type error: `input` is required
new BlockquoteControllerXstate(this, {machine: userMachine, options: {input: {userId: '123'}}});
输入可接受的机器 undefined (或者声明没有输入)可以省略 options.
actor是在构造函数中创建的,所以 snapshot, actor, send, trigger 和 can
在连接主机之前可用。它开始于 hostConnected 停了下来
hostDisconnected;如果主机再次连接,将创建一个新的参与者。与
reconnect: 'restore' 它恢复前参与者的状态(等待计时器保持其状态)
最后期限);默认情况下('restart'),它从初始状态开始。
xstate-counter.js
import {html, LitElement} from 'lit';
import {BlockquoteControllerXstate} from '@blockquote-web-components/blockquote-controller-xstate';
import {counterMachine} from './counterMachine.js';
import {styles} from './styles/xstate-counter-styles.css.js';
export class XstateCounter extends LitElement {
static styles = [styles];
constructor() {
super();
// The controller re-renders the host on every snapshot. The machine emits the element's
// public events, and `dispatchEmitted` dispatches them on it: `counterchange` with
// `detail: {counter}`, `{composed: true}` (listen on the element, or capture).
this.counterController = new BlockquoteControllerXstate(this, {
machine: counterMachine,
// Moving the element in the DOM keeps its state
reconnect: 'restore',
dispatchEmitted: true,
options: {
inspect: this._inspectEvents,
},
});
}
// xstate v6 inspection events: '@xstate.actor' | '@xstate.transition'
_inspectEvents = (inspEvent) => {
console.info('inspect event', inspEvent);
};
get #disabled() {
return this.counterController.snapshot.matches('disabled');
}
render() {
// `can.X()`: `false` when the event would be ignored (guards, `disabled` state) or the
// actor has finished. `trigger` and `can` always target the current actor.
const {can, trigger, snapshot} = this.counterController;
return html`
<slot></slot>
<div data-disabled="${this.#disabled}">
<span>
<button
?disabled="${!can.INC()}"
data-counter="increment"
\@click=${() => trigger.INC()}>
Increment
</button>
<button
?disabled="${!can.DEC()}"
data-counter="decrement"
\@click=${() => trigger.DEC()}>
Decrement
</button>
</span>
<p>${snapshot.context.counter}</p>
</div>
<div>
<button \@click=${() => trigger.TOGGLE()}>
${this.#disabled ? 'Enabled counter' : 'Disabled counter'}
</button>
</div>
`;
}
}
trigger 和 can
trigger 中生成的类型化事件 schemas.events: trigger.INC(),
trigger.RENAME({label}),就像 send({type: 'RENAME', label}). 它的签名和
XState v6的 actor.trigger,但有一点不同:XState属于一个参与者,而
控制器是一个稳定的代理(ControllerTrigger),它总是发送给当前actor。
这很重要,因为每次主机再次连接(停止)时都会创建一个新的actor
演员无法重启)。 actor.trigger 保持从第一个连接将发送到一个停止
行动者,事件就会失去; controller.trigger 可安全保存或销毁。
can (ControllerCan)有相同的形状,也是一个稳定的代理,和回报
snapshot.can(event) 对于现任演员: can.INC(), can.RENAME({label}). 它是 false
当转换函数忽略事件(返回 undefined)或者演员已经演完了
('stopped', 'done' 或者 'error'). 机器仍然拒绝无效事件: can 只有
驱动UI。
断开连接和重新连接: restart 或者 restore
一个Lit元素被断开并再次连接的频率比看起来更频繁:移动它
DOM,重新排序 repeat,改变它的父级。actor在断开连接时停止,并且
停止的actor不能重新启动,因此在重新连接时创建一个新的actor。这个 reconnect 选项
决定它从哪里开始:
reconnect | 新演员从 |
|---|---|
'restart' (默认) | 初始状态,或者 options.snapshot |
'restore' | 前一个参与者的状态:它的持久化快照 |
与 'restore',则快照在参与者停止之前被持久化
(actor.getPersistedSnapshot()),新的演员由此诞生
(createActor(machine, {snapshot})). 它使:
context,国家价值和子女。- 挂起计时器,具有其原始截止日期:在断开连接触发时过期的计时器 马上。
- 调用的工作在飞行中,如
createAsyncLogic请求:停止中止它,而新的 Actor再次运行它。
演员演完了('done' 或者 'error')重新启动,而另一个 machine 是
与此同时生效。 onSnapshot 仍然收到 'stopped' 快照断开,然后
恢复一个。
发射事件: on 和 dispatchEmitted
机器发出事件 enq.emit(...) 表示某事发生过一次:计数器
更改,提交了一个表单。中声明 schemas.emitted,元素的公共事件直播
在机器中,没有DOM的类型和可测试的。
dispatchEmitted: true 将主机上发出的每个事件分派为 CustomEvent:
发出 type 是事件名,其余是其名 detail, {composed: true} (通过一个
对象来更改它)。事件不会冒泡:监听元素本身,或者监听对象
捕获阶段的父节点({capture: true})。 EmittedEventMap 类型 addEventListener:
declare global {
interface HTMLElementEventMap extends EmittedEventMap<typeof counterMachine> {}
}
el.addEventListener('counterchange', (ev) => ev.detail.counter); // `number`
要处理代码中发出的事件,请使用 controller.on(type, handler) (或 '*'). 它有
的签名 actor.on,但XState删除 actor.on 处理程序,当参与者停止时
controller.on 处理程序不断从重新连接时创建的参与者接收事件。
交付时:
- 转换应用后:
controller.snapshot已具有新状态(主机) 尚未重新渲染)。 - 当主机断开连接(停止actor不运行)时不会
exit),当演员也是如此 恢复为reconnect: 'restore'(entry(不再运行):移动元素 DOM什么也不分派。 can.X()不触发:它只计算转换。
类中同步运行侦听器 trigger (或 send)导致事件发生;
controller.snapshot已经是新的状态,但是主机还没有重新渲染。阅读 它的渲染DOM,await el.updateComplete第一。- 一、
trigger从侦听器中调用是排队的:当当前事件完成时,它被处理; 所以controller.snapshot之后它仍然是当前状态。它自己发出的事件 跟着,按顺序。 - 抛出的侦听器被报告为未捕获的错误。其他听众仍然收到 事件,actor继续运行。
controller.trigger vs controller.actor.trigger
| 发送给 | 重新连接主机后 | |
|---|---|---|
controller.trigger | 现在的演员,每次打电话都抬头看 | 到达新演员 |
controller.actor.trigger | 读取时当前的演员 | 到达停止的参与者:事件被丢弃(开发版本中的警告) |
使用 controller.trigger 获取来自UI的事件。的请求的响应
组件,这取决于 reconnect:
'restore':新的参与者继续相同的会话,因此在 重新连接仍然有效。使用controller.trigger.'restart':新actor启动新会话,因此响应属于旧会话。读controller.actor.trigger之前await:响应被丢弃,而不是到达 新演员。
// `reconnect: 'restart'`
async #onInput(ev: InputEvent) {
const query = (ev.target as HTMLInputElement).value;
this.search.trigger.typed({query});
// Bound to this session: read before the `await`
const session = this.search.actor.trigger;
const items = await fetchSuggestions(query);
// Dropped if the actor that asked was stopped in the meantime.
// `this.search.trigger.results({items})` would show results for `query` in the new session.
session.results({items});
}
将请求移动到机器中(被调用) createAsyncLogic)避免选择:the
请求属于actor,停止actor将取消它 AbortSignal 是流产)。
与 'restore' 新演员再演一遍;与 'restart' 它随着旧的会议而消失了。
src/BlockquoteControllerXstate.ts:
类: BlockquoteControllerXstate
字段
| 名字 | 隐私 | 类型 | 默认的 | 描述 | 继承自 |
|---|---|---|---|---|---|
machine | TMachine | machine | |||
options | ControllerActorOptions<TMachine>['options'] | options | “createActor”选项。类型与构造函数参数类似,因此不能重新赋值 当机器需要输入时,没有输入(在重新连接时创建一个新的actor)。 | ||
onSnapshot | (snapshot: SnapshotFrom<TMachine>) => void | undefined | onSnapshot | |||
onError | (error: unknown) => void | undefined | onError | 是否设置它决定,当主机连接时,如果XState报告错误为未处理: 添加或删除它适用于下一个连接。 | ||
reconnect | 'restart' | 'restore' | reconnect | 当主机再次连接时读取。 | ||
actorRef | Actor<TMachine> | ||||
subscription | Subscription | undefined | ||||
currentSnapshot | SnapshotFrom<TMachine> | undefined | ||||
host | THost | ||||
trigger | ControllerTrigger<TMachine> | 从模式生成的类型化事件发送器。“:”触发事件。公司()”, ”引发的反馈。更新‘]({value}) ’,如' send({type,…有效载荷})”。 稳定代理:它总是发送给当前参与者(每次创建一个新参与者) 主机再次连接),因此可以保留或销毁它。 不是“演员。Trigger ',它绑定到一个actor。使用‘ reconnect: ’restart' ',用于事件 这属于一个会话,例如对请求的响应,read ' actor。触发之前 ‘ await ’:如果主机同时断开连接,响应将被丢弃,而不是 接触新演员。使用“restore”,会话将继续,因此使用“trigger”。 | |||
can | ControllerCan<TMachine> | 每个事件类型键入‘ snapshot.can(…)’,形状类似‘ trigger ’: “可以。公司()”、“可以['反馈。更新‘]({value}) ’。 ‘ false ’表示事件将被忽略或actor已经完成('stopped' ', 'done' ')。 或“错误的”)。在主机连接之前,它应答初始状态。 一个稳定的代理,像“触发器”:它总是检查当前的参与者。 | |||
on | Actor<TMachine>['on'] | ((type: string, handler: (event: EventObject) => void): Subscription => { const entry = {type, handler}; this.#emittedHandlers.add(entry); return { unsubscribe: () => { this.#emittedHandlers.delete(entry); }, }; }) as unknown | 处理机器发出的事件(' enq。Emit ‘),就像’ actor '。On (type, handler) ‘,带有’*' ' for 每一个事件。不同的演员。当actor停止时,其处理程序将被删除 当主机再次连接时,继续从创建的actor接收事件。 | ||
actor | Actor<TMachine> | 来自XState的底层actor。每次创建主机时都会创建一个新的actor 再次连接,所以不要跨连接保持对它的引用。 | |||
snapshot | SnapshotFrom<TMachine> | actor状态的最新快照 | |||
onNext | 状态更改的内部订阅者 | ||||
isActive | boolean | ‘ true ’,而当前actor可以(或正在)运行。 快照在start()之前和运行时都是“活动的”;后“停止()” 它是“停止”(当它完成或失败时是“完成”/“错误”)。 |
方法
| 名字 | 隐私 | 描述 | 参数 | 返回 | 继承自 |
|---|---|---|---|---|---|
send | 向actor服务发送事件 | ev: EventFrom<typeof this.machine> | void | ||
unsubscribe | void | ||||
startService | 启动演员。如果前一个参与者不再活动,则创建一个新参与者 (例如,当主机断开连接时停止),因为停止的actor无法重新启动。 | void | |||
stopService | void | ||||
hostConnected | void | ||||
hostDisconnected | void |
私有API
字段
| 名字 | 隐私 | 类型 | 默认的 | 描述 | 继承自 |
|---|---|---|---|---|---|
#dispatchInit | 私人 | DispatchEmittedOptions | undefined | ‘ dispatchemited ’解析为‘ EventInit ’,或‘ undefined ’当它关闭时。 | ||
#emittedHandlers | 私人 | new Set<{ type: string; handler: (event: EventObject) => void; }>() | |||
#emittedSubscription | 私人 | Subscription | undefined | |||
#persisted | 私人 | ”{ |
machine: TMachine;
snapshot: ActorOptions<TMachine>['snapshot'];
} |未定义 | | Persisted on disconnect with \与它所属的机器重新连接:‘restore’ '。| |
| #onEmitted | private | | |将当前actor发出的事件传递给DOM和‘ on ’处理程序。| |
方法
| 名字 | 隐私 | 描述 | 参数 | 返回 | 继承自 |
|---|---|---|---|---|---|
#createActor | 私人 | snapshot: ActorOptions<TMachine>['snapshot'] | Actor<TMachine> | ||
#takePersisted | 私人 | 在重新连接时要恢复的快照:仅使用‘ reconnect: ’restore',并且仅用于 被持久化的机器(‘ Machine ’可能已被重新分配)。 | ActorOptions<TMachine>['snapshot'] |
出口
| 种类 | 名字 | 宣言 | 模块 | 包 |
|---|---|---|---|---|
js | BlockquoteControllerXstate | BlockquoteControllerXstate | src / BlockquoteControllerXstate.ts |
src/index.ts:
出口
| 种类 | 名字 | 宣言 | 模块 | 包 |
|---|---|---|---|---|
js | BlockquoteControllerXstate | BlockquoteControllerXstate | 。/ BlockquoteControllerXstate.js | |
js | BlockquoteControllerXstateOptions | BlockquoteControllerXstateOptions | 。/ BlockquoteControllerXstate.js | |
js | ControllerCan | ControllerCan | 。/ BlockquoteControllerXstate.js | |
js | ControllerTrigger | ControllerTrigger | 。/ BlockquoteControllerXstate.js | |
js | DispatchEmittedOptions | DispatchEmittedOptions | 。/ BlockquoteControllerXstate.js | |
js | EmittedEventMap | EmittedEventMap | 。/ BlockquoteControllerXstate.js | |
js | UseMachineOptions | UseMachineOptions | 。/ BlockquoteControllerXstate.js |