Skip to content
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

Open
wants to merge 18 commits into
base: main
Choose a base branch
from

Conversation

oscarmarina
Copy link

@oscarmarina oscarmarina commented Feb 28, 2024

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

  • Provides get actor, get snapshot, and send(ev: EventFrom<TMachine>) method for interacting with the XState actor.
  • Exposes an unsubscribe method
  • Option to pass a reactive property for use in the actor's subscribe method.
  • Documentation of the API in the README file.
this.toggleController = new UseMachine(this, {
  machine: toggleMachine,
  options: { inspect },
  subscriptionProperty: '_xstate',
});

// ...

updated(props: Map<string, unknown>) {
  super.updated && super.updated(props);
  if (props.has('_xstate')) {
    const { value } = this._xstate;
    const toggleEvent = new CustomEvent('togglechange', {
      detail: value,
    });
    this.dispatchEvent(toggleEvent);
  }
}

// ...

private get _turn() {
    return this.toggleController.snapshot.matches('inactive');
  }

render() {
  return html`
    <button @click=${() => this.toggleController.send({ type: 'TOGGLE' })}>
      ${this._turn ? 'Turn on' : 'Turn off'}
    </button>
  `;
}

useActorRef.ts

Creates and returns the XState actor without Lit-specific dependencies (handled in UseMachine.js).

index.ts:

Exports only UseMachine.ts.

It is a structure more in line with the working approach used by Lit's controllers.

📂 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

it('should be able to spawn an actor from actor logic', async () => {
    const el: UseActorWithTransitionLogic = await fixture(
      html`<use-actor-with-transition-logic></use-actor-with-transition-logic>`
    );
    const buttonEl = getByTestId(el, 'count');
    await waitFor(() => expect(buttonEl.textContent?.trim()).toEqual('0'));
    await fireEvent.click(buttonEl);
    await el.updateComplete;
    await waitFor(() => expect(buttonEl.textContent?.trim()).toEqual('1'));
  });

useActorRef.test.ts

it('observer should be called with next state', async () => {
    const el: UseActorRef = await fixture(
      html`<use-actor-ref></use-actor-ref>`
    );
    const buttonEl = getByTestId(el, 'button');
    await waitFor(() => expect(buttonEl.textContent?.trim()).toBe('Turn on'));
    await fireEvent.click(buttonEl);
    await el.updateComplete;
    await waitFor(() => expect(buttonEl.textContent?.trim()).toBe('Turn off'));
  });

📂 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.
<lit-ts> <lit-ts-counter>

Update before "publish"

import { UseMachine } from '../../../packages/xstate-lit/src/index.js';
// import { UseMachine } from '@xstate/lit';
  • templates/lit-ts/src/LitTs.ts (line 4 and 5)
  • templates/lit-ts/src/LitTsCounter.ts (line 5 and 6)

Other Modified Files:

▨ jest.config.js

Add transformIgnorePatterns to accommodate Lit and Open-WC.

transformIgnorePatterns: [
  'node_modules/(?!(@open-wc|lit-html|lit-element|lit|@lit)/)'
],

📂 scripts/jest-utils/

▨ setup.js

Filters out Lit's "Lit is in dev mode..." console logs during tests.


Copy link

changeset-bot bot commented Feb 28, 2024

⚠️ No Changeset found

Latest commit: eb66d11

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Copy link

codesandbox-ci bot commented Feb 28, 2024

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.

Copy link
Member

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

Copy link
Author

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

Copy link
Member

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

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

Comment on lines +8 to +10
transformIgnorePatterns: [
'node_modules/(?!(@open-wc|lit-html|lit-element|lit|@lit)/)'
],
Copy link
Member

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?

Copy link
Author

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.

jestjs/jest#11783 (comment)

===
jest-and-lit

@oscarmarina
Copy link
Author

Hi, @davidkpiano, @Andarist
I have uploaded to stackblitz "a version of the demo located in the template folder" if you want to see how it works.

The folder xstat-lit corresponds to what is in the folder packages/xstate-lit

oscarmarina and others added 9 commits March 3, 2024 15:33
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 <mateuszburzynski@gmail.com>

* Update packages/core/src/actions/emit.ts

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>

* Update packages/core/src/State.ts

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>

* Update packages/core/src/actions/emit.ts

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>

* Update packages/core/src/actions/emit.ts

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>

* Update packages/core/src/actions/emit.ts

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>

* Update packages/core/test/types.test.ts

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>

* Update packages/core/src/createMachine.ts

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>

* Update packages/core/src/actions/emit.ts

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>

* 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 <mateuszburzynski@gmail.com>
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>
Comment on lines 3 to 9
export const useActorRef = <TMachine extends AnyActorLogic>(
logic: TMachine,
options?: ActorOptions<TMachine>
): Actor<TMachine> => {
const actorRef = createActor(logic, options);
return actorRef;
};
Copy link
Member

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?

Copy link
Author

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.

Comment on lines 19 to 21
fetchController: UseMachine<typeof fetchMachine> = {} as UseMachine<
typeof fetchMachine
>;
Copy link
Member

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

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, updated.

@oscarmarina
Copy link
Author

Hi @Andarist
I'm thinking that if the "PR" is correct, I would need to decline this PR and create two new ones, one for @xstate/lit and another for the templates/lit-ts.

This way, I can add the dependency to the templates/lit-ts package file and also update the "import(s)".

Here are the steps I would take:

  • Decline the current PR.
  • Create a new PR for "@xstate/lit".
  • Create a new PR for the "templates/lit-ts".
    • In the "templates/lit-ts" package file, add the dependency on "@xstate/lit".
    • In the "templates/lit-ts" update the "import(s)" to use "@xstate/lit".
  • Submit the two new PRs for review.

Does this approach sound reasonable?

@Andarist
Copy link
Member

Andarist commented Mar 8, 2024

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 @xstate/lit then you'll be able to open a new one with the templates.

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!

@oscarmarina
Copy link
Author

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.

@oscarmarina
Copy link
Author

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!

@christophe-g
Copy link

christophe-g commented Apr 12, 2024

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>
   `;
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants