Skip to content

Instantly share code, notes, and snippets.

@pigoz
Last active September 4, 2024 11:19
Show Gist options
  • Save pigoz/57636c112be44a6871c0b14dfbe77261 to your computer and use it in GitHub Desktop.
Save pigoz/57636c112be44a6871c0b14dfbe77261 to your computer and use it in GitHub Desktop.
TypeScript Stimulus Controller
import { TController } from "./TController";
import FooExample2 from "./foo_example2_controller";
export default class extends TController({
el: HTMLDivElement,
values: {
foo: String,
bar: Boolean,
},
targets: {
button: HTMLButtonElement,
},
outlets: {
"foo-example2": FooExample2,
},
}) {
connect() {
this.element satisfies HTMLDivElement;
this.fooValue satisfies string;
this.hasFooValue satisfies boolean;
this.barValue satisfies boolean;
this.hasBarValue satisfies boolean;
this.buttonTarget satisfies HTMLButtonElement;
this.hasButtonTarget satisfies boolean;
this.fooExample2Outlet.customMethod() satisfies "!";
this.fooExample2Outlet.hasBazValue satisfies boolean;
this.fooExample2Outlet.bazValue satisfies boolean;
console.log("hello from example controller", {
el: this.element,
button: this.buttonTarget,
foo: this.fooValue,
bar: this.barValue,
});
// {el: div, button: button.btn.btn-primary, foo: 'foo', bar: false}
}
}
import { TController } from "./TController";
export default class extends TController({
el: HTMLAnchorElement,
values: {
baz: Boolean,
},
}) {
connect(): void {
this.element.textContent = "Outlet content set by Stimulus";
}
customMethod() {
return "!" as const;
}
}
<div
data-controller="foo-example1"
data-foo-example1-foo-value="foo"
data-foo-example1-bar-value="false"
data-foo-example1-foo-example2-outlet="#out"
>
<button type="button" class="btn btn-primary" data-foo-example1-target="button">Button</button>
</div>
<a data-controller="foo-example2" id="out">
Outlet
</a>
import { Context, Controller } from "@hotwired/stimulus";
interface PClass<T> {
prototype: T;
}
// Camelback
type Cbz<T extends string> = T extends `${infer A}_${infer B}`
? `${A}${Cbz<Capitalize<B>>}`
: T extends `${infer A}-${infer B}`
? `${A}${Cbz<Capitalize<B>>}`
: T;
type Camelize<T extends string> = Capitalize<Cbz<T>>;
namespace Values {
type ValuesConstructors =
| BooleanConstructor
| NumberConstructor
| StringConstructor;
interface TypeWithDefault {
type: ValuesConstructors;
default: unknown;
}
export interface ValueType {
[name: string]: ValuesConstructors | TypeWithDefault;
}
type HasValue<V extends ValueType> = {
[K in keyof V as `has${Camelize<K & string>}Value`]: boolean;
};
type NamedValue<V extends ValueType> = {
[K in keyof V as `${Cbz<K & string>}Value`]: V[K] extends TypeWithDefault
? ReturnType<V[K]["type"]>
: V[K] extends ValuesConstructors
? ReturnType<V[K]>
: unknown;
};
export type Typed<V extends ValueType> = HasValue<V> & NamedValue<V>;
}
namespace Targets {
type TargetConstructors = PClass<HTMLElement>;
export interface ValueType {
[name: string]: TargetConstructors;
}
type HasTarget<V extends ValueType> = {
[K in keyof V as `has${Camelize<K & string>}Target`]: boolean;
};
type NamedTarget<V extends ValueType> = {
[K in keyof V as `${Cbz<
K & string
>}Target`]: V[K] extends TargetConstructors ? V[K]["prototype"] : unknown;
};
export type Typed<V extends ValueType> = HasTarget<V> & NamedTarget<V>;
}
namespace Outlets {
type OutletConstructors = PClass<Controller>;
export interface ValueType {
[name: string]: OutletConstructors;
}
type HasOutlet<V extends ValueType> = {
[K in keyof V as `has${Camelize<K & string>}Outlet`]: boolean;
};
type NamedOutlet<V extends ValueType> = {
[K in keyof V as `${Cbz<
K & string
>}Outlet`]: V[K] extends OutletConstructors ? V[K]["prototype"] : unknown;
};
export type Typed<V extends ValueType> = HasOutlet<V> & NamedOutlet<V>;
}
interface StaticOptions<
V extends Values.ValueType,
T extends Targets.ValueType,
O extends Outlets.ValueType
> {
values?: V;
targets?: T;
outlets?: O;
}
interface TControllerInstance<
P extends PClass<any>,
V extends Values.ValueType,
T extends Targets.ValueType,
O extends Outlets.ValueType
> {
new (context: Context): Controller<P["prototype"]> &
Values.Typed<V> &
Targets.Typed<T> &
Outlets.Typed<O>;
}
export function TController<
E extends HTMLElement,
P extends PClass<E>,
V extends Values.ValueType = {},
T extends Targets.ValueType = {},
O extends Outlets.ValueType = {}
>(options: StaticOptions<V, T, O> & { el: P }) {
return class extends Controller<E> {
static values = options.values ?? {};
static targets = Object.keys(options.targets ?? {});
static outlets = Object.keys(options.outlets ?? {});
} as unknown as TControllerInstance<P, V, T, O>;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment