mirror of
https://github.com/storytold/storyteller-bevy.git
synced 2026-10-17 00:13:17 +00:00
UI styling, main workspace layout (#119)
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
export * from "./decorators";
|
||||
export * from "./events";
|
||||
export * from "./observe";
|
||||
export * from "./types";
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { LitElement, PropertyDeclaration, PropertyValues, html, render } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { createRef, ref } from "lit/directives/ref.js";
|
||||
|
||||
import { inject, provide } from "./decorators";
|
||||
import { observe } from "./observe";
|
||||
import { DynamicProvider, UniqueToken } from "./types";
|
||||
|
||||
interface CountProvider extends DynamicProvider<CountProvider> {
|
||||
readonly count: number;
|
||||
}
|
||||
|
||||
const COUNT_PROVIDER = UniqueToken.create<CountProvider>();
|
||||
|
||||
@customElement("test-count-provider")
|
||||
@provide(COUNT_PROVIDER)
|
||||
class TestCountProvider extends LitElement implements CountProvider {
|
||||
@state() count = 0;
|
||||
|
||||
protected override update(changes: PropertyValues<this>): void {
|
||||
if (changes.has("count")) {
|
||||
const downstreamChanges: PropertyValues<CountProvider> = new Map();
|
||||
downstreamChanges.set("count", changes.get("count"));
|
||||
|
||||
this.dispatchEvent(new CustomEvent("property-changes", {
|
||||
detail: downstreamChanges,
|
||||
}));
|
||||
}
|
||||
super.update(changes);
|
||||
}
|
||||
|
||||
protected override render = () => html`
|
||||
<slot></slot>
|
||||
`;
|
||||
}
|
||||
|
||||
@customElement("test-count-dependent")
|
||||
class TestCountDependent extends LitElement {
|
||||
@observe(["count"])
|
||||
@inject(COUNT_PROVIDER)
|
||||
countProvider!: CountProvider;
|
||||
|
||||
override requestUpdate(
|
||||
name?: PropertyKey | undefined,
|
||||
oldValue?: unknown,
|
||||
options?: PropertyDeclaration<unknown, unknown> | undefined
|
||||
): void {
|
||||
super.requestUpdate(name, oldValue, options);
|
||||
}
|
||||
|
||||
protected override update(changes: PropertyValues<this>): void {
|
||||
this.setAttribute("count", this.countProvider?.count?.toString() ?? null);
|
||||
super.update(changes);
|
||||
}
|
||||
}
|
||||
|
||||
describe("@observe", () => {
|
||||
it("should work", async () => {
|
||||
const providerRef = createRef<TestCountProvider>();
|
||||
const dependentRef = createRef<TestCountDependent>();
|
||||
|
||||
render(html`
|
||||
<test-count-provider ${ref(providerRef)}>
|
||||
<test-count-dependent ${ref(dependentRef)}></test-count-dependent>
|
||||
</test-count-provider>
|
||||
`, document.body);
|
||||
|
||||
expect(providerRef.value).toBeDefined();
|
||||
expect(dependentRef.value).toBeDefined();
|
||||
|
||||
while (!(await dependentRef.value!.updateComplete));
|
||||
expect(dependentRef.value!.getAttribute("count")).toEqual("0");
|
||||
|
||||
providerRef.value!.count = 42;
|
||||
while (!(await providerRef.value!.updateComplete));
|
||||
while (!(await dependentRef.value!.updateComplete));
|
||||
expect(dependentRef.value!.getAttribute("count")).toEqual("42");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
import { LitElement, PropertyValues } from "lit";
|
||||
import { appendRoutine, prependRoutine } from "../decorators/internal";
|
||||
import { DynamicProvider } from "./types";
|
||||
|
||||
/**
|
||||
* Observe the given property names of the decorated {@linkcode DynamicProvider},
|
||||
* calling `requestUpdate` to re-render the component when one of those
|
||||
* properties is updated in the provider.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const COUNT_PROVIDER = UniqueToken.create<CountProvider>();
|
||||
*
|
||||
* interface CountProvider extends DynamicProvider<CountProvider> {
|
||||
* readonly count: number;
|
||||
* }
|
||||
*
|
||||
* \@customElement("sts-count-observer")
|
||||
* export class CountObserverElement extends LitElement {
|
||||
* \@observe(["count"])
|
||||
* \@inject(COUNT_PROVIDER)
|
||||
* _countProvider!: CountProvider;
|
||||
*
|
||||
* protected override render = () => html`
|
||||
* <!--
|
||||
* This component should correctly update whenever the injected
|
||||
* `CountProvider`'s `count` property changes.
|
||||
* -->
|
||||
* The count is: ${this._countProvider.count}
|
||||
* `;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function observe<
|
||||
T extends LitElement,
|
||||
K extends keyof T,
|
||||
P extends (T[K] & object),
|
||||
>(observedProps: (keyof P)[])
|
||||
: T[K] extends DynamicProvider<T[K]>
|
||||
? ((proto: T, propName: K) => void)
|
||||
: never
|
||||
{
|
||||
return ((proto: T, propName: K) => {
|
||||
function onProviderChanges(
|
||||
this: T,
|
||||
{ detail: changes }: CustomEvent<PropertyValues<P>>,
|
||||
): void {
|
||||
// FIXME: Holy shit TypeScript, gg
|
||||
if (observedProps.some(prop => changes.has(prop as never)))
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
const $onProviderChanges = Symbol("onProviderChanges");
|
||||
|
||||
type Decorated = T & {
|
||||
[$onProviderChanges]: typeof onProviderChanges;
|
||||
}
|
||||
|
||||
appendRoutine(proto, "connectedCallback", function (this: T) {
|
||||
assertType<Decorated>(this);
|
||||
this[$onProviderChanges] ??= onProviderChanges.bind(this);
|
||||
|
||||
const provider = this[propName];
|
||||
assertType<DynamicProvider<P>>(provider);
|
||||
|
||||
provider?.addEventListener?.("property-changes", this[$onProviderChanges]);
|
||||
});
|
||||
|
||||
prependRoutine(proto, "disconnectedCallback", function (this: T) {
|
||||
assertType<Decorated>(this);
|
||||
|
||||
const provider = this[propName];
|
||||
assertType<DynamicProvider<P>>(provider);
|
||||
|
||||
provider?.removeEventListener?.("property-changes", this[$onProviderChanges]);
|
||||
});
|
||||
}) as any
|
||||
}
|
||||
|
||||
function assertType<T>(value: unknown): asserts value is T {}
|
||||
Reference in New Issue
Block a user