Skip to content

Commit

Permalink
Merge pull request #1841 from NullVoxPopuli/xstate-5
Browse files Browse the repository at this point in the history
Upgrade to XState 5
  • Loading branch information
NullVoxPopuli authored Oct 20, 2024
2 parents 4069dd9 + 239b90c commit 00a0ddd
Show file tree
Hide file tree
Showing 9 changed files with 1,025 additions and 536 deletions.
5 changes: 1 addition & 4 deletions apps/repl/app/app.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'limber-ui/theme.css';
import 'ember-statechart-component';

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
Expand All @@ -10,8 +11,6 @@ import { _backburner } from '@ember/runloop';

import loadInitializers from 'ember-load-initializers';
import Resolver from 'ember-resolver';
import { setupComponentMachines } from 'ember-statechart-component';
import { StateNode } from 'xstate';

import config from 'limber/config/environment';

Expand Down Expand Up @@ -41,5 +40,3 @@ export default class App extends Application {
}

loadInitializers(App, config.modulePrefix);

setupComponentMachines(StateNode);
63 changes: 55 additions & 8 deletions apps/repl/app/components/limber/editor/index.gts
Original file line number Diff line number Diff line change
@@ -1,19 +1,66 @@
import { waitForPromise } from '@ember/test-waiters';

import { resource, resourceFactory } from 'ember-resources';
import { TrackedObject } from 'tracked-built-ins';

import { service } from 'limber-ui';

import codemirror from './-code-mirror';
import codemirror, { setupCodeMirror } from './-code-mirror';
import Loader from './loader';
import { LoadingError } from './loading-error';
import { Placeholder } from './placeholder';
import State from './state';

import type { TOC } from '@ember/component/template-only';

function deferCodemirror() {
let state = new TrackedObject({ isLoading: false, isDone: false, error: null });

function getEditor() {
state.isLoading = true;
waitForPromise(setupCodeMirror())
.then(() => {
state.isDone = true;
})
.catch((error) => (state.error = error))
.finally(() => {
state.isLoading = false;
});
}

function cleanup() {
window.removeEventListener('mousemove', load);
window.removeEventListener('keydown', load);
window.removeEventListener('touchstart', load);
}

let load = () => {
getEditor();
cleanup();
};

return resource(({ on, owner }) => {
if (owner.lookup('service:router').currentRoute?.queryParams?.['forceEditor'] === 'true') {
load();
}

on.cleanup(() => cleanup());

window.addEventListener('mousemove', load, { passive: true });
window.addEventListener('keydown', load, { passive: true });
window.addEventListener('touchstart', load, { passive: true });

return state;
});
}

resourceFactory(deferCodemirror);

export const Editor: TOC<{
Element: HTMLDivElement;
}> = <template>
<State as |state|>
{{#let (deferCodemirror) as |state|}}

{{#if (state.matches "editingWithCodeMirror")}}
{{#if state.isDone}}

{{#let (service "editor") as |context|}}
<div class="overflow-hidden overflow-y-auto">
Expand All @@ -28,19 +75,19 @@ export const Editor: TOC<{
...attributes
>

{{#if (state.matches "loadCodeMirror")}}
{{#if state.isLoading}}
<Loader />
{{/if}}

{{#if (state.matches "error")}}
<LoadingError @error={{state.context.error}} />
{{#if state.error}}
<LoadingError @error={{state.error}} />
{{/if}}

<Placeholder />
</div>
{{/if}}

</State>
{{/let}}
</template>;

export default Editor;
67 changes: 0 additions & 67 deletions apps/repl/app/components/limber/editor/state.ts

This file was deleted.

47 changes: 30 additions & 17 deletions apps/repl/app/components/limber/layout/index.gts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
// need to Fix something in ember-statechart-component
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
import { assert } from '@ember/debug';
import { fn, hash } from '@ember/helper';
import { fn } from '@ember/helper';

import { modifier } from 'ember-modifier';

Expand All @@ -11,17 +8,21 @@ import { EditorContainer, OutputContainer } from './containers';
import { Controls } from './controls';
import { Orientation } from './orientation';
import { ResizeHandle } from './resize-handle';
import State, { isHorizontalSplit, setupResizeObserver } from './state';
import { isHorizontalSplit, LayoutState, setupResizeObserver } from './state';

import type { TOC } from '@ember/component/template-only';
import type { Send, State as StateFor } from 'ember-statechart-component/glint';
import type { ReactiveActorFrom } from 'ember-statechart-component';

const setupState = modifier((element: Element, [send]: [Send<unknown>]) => {
type ReactiveActor = ReactiveActorFrom<typeof LayoutState>;

const setupState = modifier((element: Element, [send]: [(event: string) => void]) => {
assert(`Element is not resizable`, element instanceof HTMLElement);

let observer = setupResizeObserver(() => send('RESIZE'));

send('CONTAINER_FOUND', {
// @ts-expect-error need to fix the type of this for ember-statechart-component
send({
type: 'CONTAINER_FOUND',
container: element,
observer,
maximize: () => send('MAXIMIZE'),
Expand All @@ -37,46 +38,58 @@ const effect = (fn: (...args: unknown[]) => void) => {
fn();
};

const isResizable = (state: StateFor<typeof State>) => {
const isResizable = (state: ReactiveActor) => {
return !(state.matches('hasContainer.minimized') || state.matches('hasContainer.maximized'));
};

/**
* true for horizontally split
* false for vertically split
*/
const containerDirection = (state: StateFor<typeof State>) => {
const containerDirection = (state: ReactiveActor) => {
if (state.matches('hasContainer.default.horizontallySplit')) {
return true;
}

return isHorizontalSplit(state.context);
return isHorizontalSplit(state.snapshot);
};

function updateOrientation(isVertical: boolean) {
return {
type: 'ORIENTATION',
isVertical,
};
}

export const Layout: TOC<{
Blocks: {
editor: [];
output: [];
};
}> = <template>
<State as |state send|>
<LayoutState as |state|>
{{#let (containerDirection state) as |horizontallySplit|}}
<Orientation as |isVertical|>
{{effect (fn send "ORIENTATION" (hash isVertical=isVertical))}}
{{! Normally we don't do effects in app code,
because we can derive all state.
But XState is an *evented* system, so we have to send events.
}}
{{effect (fn state.send (updateOrientation isVertical))}}

<div
{{! row = left to right, col = top to bottom }}
class="{{if horizontallySplit 'flex-col' 'flex-row'}} flex overflow-hidden"
>

<EditorContainer @splitHorizontally={{horizontallySplit}} {{setupState send}}>
<EditorContainer @splitHorizontally={{horizontallySplit}} {{setupState state.send}}>
<Save />
<Controls
@isMinimized={{state.matches "hasContainer.minimized"}}
@isMaximized={{state.matches "hasContainer.maximized"}}
@needsControls={{toBoolean state.context.container}}
@needsControls={{toBoolean state.snapshot.context.container}}
@splitHorizontally={{horizontallySplit}}
@send={{send}}
@send={{state.send}}
/>

{{yield to="editor"}}
Expand All @@ -100,7 +113,7 @@ export const Layout: TOC<{
</div>
</Orientation>
{{/let}}
</State>
</LayoutState>
</template>;

export default Layout;
Loading

0 comments on commit 00a0ddd

Please sign in to comment.