-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[@xstate/lit] Add Lit Controller #4775
base: main
Are you sure you want to change the base?
Conversation
|
This pull request is automatically built and testable in CodeSandbox. To see build info of the built libraries, click here or the icon next to each commit SHA. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this package should try to use this experimental option: preconstruct/preconstruct#586
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi, I added 'type:module' and that fixed the error in "yarn typecheck".
https://github.com/preconstruct/preconstruct/pull/586/checks#discussion_r1452789690
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it still should use that experimental option - otherwise, I'd expect us to run into problems with preconstruct dev/validate/build
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
transformIgnorePatterns: [ | ||
'node_modules/(?!(@open-wc|lit-html|lit-element|lit|@lit)/)' | ||
], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why do we need this? does it turn off ESM->CJS transform?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is necessary to use Jest with Lit.
You can see more information here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Jest still doesn't support modules??
Hi, @davidkpiano, @Andarist The folder xstat-lit corresponds to what is in the folder packages/xstate-lit |
export const useActorRef = <TMachine extends AnyActorLogic>( | ||
logic: TMachine, | ||
options?: ActorOptions<TMachine> | ||
): Actor<TMachine> => { | ||
const actorRef = createActor(logic, options); | ||
return actorRef; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's just a wrapper around createActor
- do you even need it here? couldn't u just call createActor
directly in ur UseMachine
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Completely agree, I did it like this following the 'format' of the other packages, but it's much better to use 'createActor' directly.
Done.
packages/xstate-lit/test/UseActor.ts
Outdated
fetchController: UseMachine<typeof fetchMachine> = {} as UseMachine< | ||
typeof fetchMachine | ||
>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wouldn't it be possible to call new UseMachine(...)
here? declaring a property like this would be much easier for consumers
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, updated.
Hi @Andarist This way, I can add the dependency to the Here are the steps I would take:
Does this approach sound reasonable? |
I'd keep this PR open but remove templates from it. Then you can have a branch with templates on it, and point us to it so we can see how it's used in practice (although tests in this PR here might/should be enough too). Once we land this PR that introduces Also, please note that I'm aware of this PR and I plan to review it thoroughly (so far I didn't really do a proper review - just a driveby one). It might take some time before I properly get to it because at the moment I'm focusing on something else. We really appreciate your contribution! |
Perfect, I'll leave it as it is for now to not drive you crazy, and if it eventually makes sense and gets approved, I'll make the necessary changes. Thank you for taking the time to review the PR. |
Hi @Andarist, just checking in to see if there's been any progress on reviewing the PR or if you need me to make any changes. Thanks! |
For those interested, this is an alternate lit-controller: https://github.com/lit-apps/lit-app/tree/dev/packages/actor It leverages https://github.com/lit-apps/lit-app/tree/dev/packages/state and wraps xstate actors so that lit element re-renders when the snapshot changes. example usage: const actor = new Actor(workflow)
export default class fsmTest extends LitElement {
// bind actor state to fsmTest element, so it will re-render when actor snapshot changes
bindActor = new StateController(this, actor)
override render() {
const send = () => actor.send({
type: 'NewPatientEvent', name: 'John', condition: 'healthy'
})
return html`
<div>
<div>status: ${actor.status}</div>
<div>any context value: ${actor.context.anyValue}</div>
<div>value: ${JSON.stringify(actor.value)}</div>
</div>
<button @click=${send}>NewPatientEvent</button>
`;
}
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is great! Hope a drive-by review is welcome!
Subscription | ||
} from 'xstate'; | ||
|
||
export class UseMachine<TMachine extends AnyStateMachine> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is a controller and not a React hook, I'd recommend against the "Use" name, and call this something like MachineController
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @justinfagnani
I use "Use" to follow the naming style as closely as possible to xstate. I took packages xstate-vue and xstate-svelte as references. Even though they are not related to React, they use similar nomenclature.
How do you see it, @Andarist?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As someone more planted in the Lit ecosystem, I think it'd be nice to match Lit naming too - which just follows the somewhat cross-language standard OO noun-phrase vs verb-phrase naming. The class should be the noun-phrase name of the category of thing that the instances are, while functions should be verb-phrases.
UseMachine
is a verb-phrase, and I think it reads a bit weird to have statements like:
this.fetchController = new UseMachine(...);
Because this is saying that fetchController
is a "UseMachine". But what is a "UseMachine"? It's a controller - an object with a verb-phrased name. But it sounds like a function.
This is why most reactive controllers are named {X}Controller.
It's possible to vend a function that makes the class for you, so maybe that's better:
this.fetchController = useMachine(...);
But even then, in the Lit ecosystem I'd still want to shy away from the "use" name because people tend to think that it's a hook and has React-like "rule of hooks" semantics, when it doesn't.
export class UseMachine<TMachine extends AnyStateMachine> | ||
implements ReactiveController | ||
{ | ||
private host: ReactiveControllerHost; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd recommend prefixing private fields with _
so that they're more obviously private even to plain JavaScript users.
Even better if you can, I'd use standard private fields. They have very good browser support these days, and even better tool support.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As with my previous comment, I followed the style used in xstate here. I reviewed some controllers in Lit, such as:
And decided to maintain that style.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops... those don't actually follow our code style, so I'll fix them. Thanks for pointing them out!
One reason why we prefix all private fields is so that they can be renamed in a minifier like Terser with a simple rule.
return html` | ||
<slot></slot> | ||
<div> | ||
${this.fetchController.snapshot.matches('idle') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know the XState API, but could the control offer a @lit/task
-like render helper that's a bit more declarative?
Something like:
this.fetchController.render({
idle: () => html`...`,
loading: () => html`...`,
success: () => html`...`,
});
Could such an API be type safe wrt the the state names?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I'm not quite sure if those two concepts can be combined. We would need @davidkpiano and @Andarist 's input on this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Which two concepts?
${this.fetchController.snapshot.matches('idle') | ||
? html` | ||
<button | ||
@click=${() => this.fetchController.send({ type: 'FETCH' })} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This may be a super small DX improvement, but the controller could offer a helper that created an event handler for the user, instead of requiring an arrow function. It would also eliminate the closure creation on every render for a small perf gain.
Something like
html`
<button
@click=${this.fetchController.sendHandler({ type: 'FETCH' })}
sendHandler()
is probably a terrible name, but I couldn't think of something better right now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’m not quite sure what you're suggesting here. Could you please clarify?
Here is a repository where I created some demos before making the PR. If possible, you can add your suggestions there:
https://github.com/oscarmarina/XstateController/blob/main/xstate-lit/src/UseMachine.ts
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So right now you need to use an arrow function in the click handler:
html`
<button
@click=${() => this.fetchController.send({ type: 'FETCH' })}
>
Fetch
</button>
`
This is fine, but it does allocate a new function object for this binding every render, and it's a slight bit of boilerplate.
Instead you can have the controller allocate a function object once and reuse it every render.
Using it would look like:
html`
<button
@click=${this.fetchController.sendHandler({ type: 'FETCH' })}
>
Fetch
</button>
`
and implementing it would look like:
class MachineController {
#sendHandler;
/** Returns a callback that calls this.send() with the given arguments **/
sendHandler(...args) {
return this.#sendHandler ??= () => this.send(...args);
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi,
I have a couple of doubts:
With this approach, <button @click=${this.fetchController.sendHandler({ type: 'FETCH' })}>Fetch</button>
, the function is called on each render pass.
so "null assignment" is a problem and caches the function:
// the function is invoked as soon as the template is processed, rather than in response to a click event.
sendHandlerCache(ev: EventFrom<TMachine>) {
console.log('#sendHandler::', ev);
// the use of "nullish assignment (??=)" causes it to be assigned only once
return (this.#sendHandler ??= () => this.sendEventFrom(ev));
}
Without the "nullish assignment", it works perfectly:
// the function is invoked as soon as the template is processed, rather than in response to a click event.
sendHandler(ev: EventFrom<TMachine>) {
console.log('#sendHandlerNoNullish::', ev);
// In this way it works, but does it make sense to create a class field?
return (this.#sendHandlerNoNullish = () => this.sendEventFrom(ev));
}
but it seems unnecessary when it can be done like this:
// the function is invoked as soon as the template is processed, rather than in response to a click event.
send(ev: EventFrom<TMachine>) {
console.log('send - arrow function::', ev);
// It works, the question is does it improve the DX?
// And does it eliminate the closure creation on every render for a small performance gain?
return () => this.sendEventFrom(ev);
}
sendEventFrom(ev: EventFrom<TMachine>) {
console.log('click::', ev);
this.actorRef?.send(ev);
}
Does this last approach eliminate the creation of closures on every render?
It's true that, from a DX perspective, avoiding the need for arrow functions is better.
1. state renamed to snapshot; 2. transition event should in an object.
* Support for parameterized `enqueueActions` * add missing context
* Add basic event emitter * Remove id and delay * Fix types * Rename * Add machine types * Add TEmitted type... everywhere * Avoid upsetting devs who rely on order of ActorLogic<…> generics * Same for ActorScope<…> * Update packages/core/src/actions/emit.ts Co-authored-by: Mateusz Burzyński <[email protected]> * Update packages/core/src/actions/emit.ts Co-authored-by: Mateusz Burzyński <[email protected]> * Update packages/core/src/State.ts Co-authored-by: Mateusz Burzyński <[email protected]> * Update packages/core/src/actions/emit.ts Co-authored-by: Mateusz Burzyński <[email protected]> * Update packages/core/src/actions/emit.ts Co-authored-by: Mateusz Burzyński <[email protected]> * Update packages/core/src/actions/emit.ts Co-authored-by: Mateusz Burzyński <[email protected]> * Update packages/core/test/types.test.ts Co-authored-by: Mateusz Burzyński <[email protected]> * Update packages/core/src/createMachine.ts Co-authored-by: Mateusz Burzyński <[email protected]> * Update packages/core/src/actions/emit.ts Co-authored-by: Mateusz Burzyński <[email protected]> * Fix TS error * Add emit to enqueueActions * Add default * Wrap handler * Check for errors * Add changeset * Types * small tweaks * fix types * tweak things * fix small issues around listeners management * rename stuff * tighten up one default * remove unused type * fixed `MachineImplementationsActions` * No need for defer * Add test * rewrite test to make it fail correctly * defer again * Add jsdocs * Update packages/core/src/actions/emit.ts --------- Co-authored-by: Mateusz Burzyński <[email protected]>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
dfcbce1
to
d44d4a1
Compare
This pull request adds compatibility for linking XState with Lit.
Motivation
XState's state machines offer a structured and predictable approach to handle complex logic, while Lit facilitates reactive UI updates in response to state changes.
Changes
It follows the established structure of the xstate repository, including:
📂 packages/xstate-lit/src/
Adds xstate-lit to packages with code, tests, and documentation.
▨ UseMachine.ts
Implements the @xstate/lit controller, referencing other packages for guidance as much as possible.
@xstate/svelte, @xstate/vue and Lifecycle: reactive controller adapters for other frameworks
actor
, getsnapshot
, andsend(ev: EventFrom<TMachine>)
method for interacting with the XState actor.unsubscribe
method▨ useActorRef.ts
Creates and returns the XState actor without Lit-specific dependencies (handled in UseMachine.js).
▨ index.ts:
Exports only UseMachine.ts.
📂 packages/xstate-lit/test
Leverages @open-wc/testing-helpers for unit testing components, drawing inspiration from existing tests in Svelte and Vue integrations.
▨ useActor.test.ts
▨ useActorRef.test.ts
📂 templates/lit-ts/
Adds examples and documentation.
Usage:
npm i && npm start
Provides two demos:
<lit-ts>
& feedbackMachine: Equal to existing templates in other packages.<lit-ts-counter>
& counterMachine: Illustrates using a reactive property and the inspect API to listen for events that caused transitions and reset reactive property.Other Modified Files:
▨ jest.config.js
Add transformIgnorePatterns to accommodate Lit and Open-WC.
📂 scripts/jest-utils/
▨ setup.js
Filters out Lit's "Lit is in dev mode..." console logs during tests.