@pechynho/stimulus-typescript 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +28 -0
- package/README.md +275 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +6 -0
- package/dist/portal-controller.d.ts +54 -0
- package/dist/portal-controller.js +792 -0
- package/dist/portal.d.ts +13 -0
- package/dist/portal.js +101 -0
- package/dist/resolvable.d.ts +28 -0
- package/dist/resolvable.js +54 -0
- package/dist/test.d.ts +1 -0
- package/dist/test.js +56 -0
- package/dist/typed-stimulus.d.ts +78 -0
- package/dist/typed-stimulus.js +139 -0
- package/dist/typed.d.ts +69 -0
- package/dist/typed.js +60 -0
- package/dist/utils.d.ts +6 -0
- package/dist/utils.js +38 -0
- package/package.json +40 -0
- package/src/index.ts +6 -0
- package/src/portal-controller.ts +821 -0
- package/src/portal.ts +110 -0
- package/src/resolvable.ts +65 -0
- package/src/test.ts +72 -0
- package/src/typed.ts +178 -0
- package/src/utils.ts +41 -0
package/src/portal.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import {Controller} from "@hotwired/stimulus";
|
|
2
|
+
import Portal from "./portal-controller";
|
|
3
|
+
|
|
4
|
+
type Constructor<T = {}> = new (...args: any[]) => T;
|
|
5
|
+
|
|
6
|
+
export function Portals<Base extends Constructor<Controller>>(Base: Base) {
|
|
7
|
+
let outlets = (Base as any).outlets;
|
|
8
|
+
if (typeof outlets === 'undefined') {
|
|
9
|
+
outlets = [];
|
|
10
|
+
} else if (!Array.isArray(outlets)) {
|
|
11
|
+
throw new Error('Outlets must be an array');
|
|
12
|
+
}
|
|
13
|
+
if (!outlets.includes('portal')) {
|
|
14
|
+
outlets.push('portal');
|
|
15
|
+
}
|
|
16
|
+
let values = (Base as any).values;
|
|
17
|
+
if (typeof values === 'undefined') {
|
|
18
|
+
values = {};
|
|
19
|
+
} else if (typeof values !== 'object') {
|
|
20
|
+
throw new Error('Values must be an object');
|
|
21
|
+
}
|
|
22
|
+
if (typeof values['portalSelectors'] === 'undefined') {
|
|
23
|
+
values['portalSelectors'] = {
|
|
24
|
+
type: Array,
|
|
25
|
+
default: [],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const derived = class extends Base
|
|
29
|
+
{
|
|
30
|
+
static outlets = outlets;
|
|
31
|
+
static values = values;
|
|
32
|
+
|
|
33
|
+
constructor(...args: any[]) {
|
|
34
|
+
super(...args);
|
|
35
|
+
|
|
36
|
+
const portalOutlets: Set<Portal> = new Set();
|
|
37
|
+
|
|
38
|
+
const originalDisconnect = (this as any).disconnect;
|
|
39
|
+
(this as any).disconnect = function (): void {
|
|
40
|
+
if (portalOutlets.size > 0) {
|
|
41
|
+
for (const outlet of portalOutlets) {
|
|
42
|
+
outlet.unsync(this);
|
|
43
|
+
}
|
|
44
|
+
portalOutlets.clear();
|
|
45
|
+
}
|
|
46
|
+
if (typeof originalDisconnect === 'function') {
|
|
47
|
+
originalDisconnect.call(this);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const originalPortalOutletConnected = (this as any).portalOutletConnected;
|
|
52
|
+
(this as any).portalOutletConnected = function (outlet: Portal, element: HTMLElement): void {
|
|
53
|
+
outlet.sync(this);
|
|
54
|
+
portalOutlets.add(outlet);
|
|
55
|
+
if (typeof originalPortalOutletConnected === 'function') {
|
|
56
|
+
originalPortalOutletConnected.call(this, outlet, element);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const originalPortalOutletDisconnected = (this as any).portalOutletDisconnected;
|
|
61
|
+
(this as any).portalOutletDisconnected = function (outlet: Portal, element: HTMLElement): void {
|
|
62
|
+
outlet.unsync(this);
|
|
63
|
+
portalOutlets.delete(outlet);
|
|
64
|
+
if (typeof originalPortalOutletDisconnected === 'function') {
|
|
65
|
+
originalPortalOutletDisconnected.call(this, outlet, element);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const originalPortalSelectorsValueChanged = (this as any).portalSelectorsValueChanged;
|
|
70
|
+
(this as any).portalSelectorsValueChanged = function (value: string[], previousValue: string[]): void {
|
|
71
|
+
const outletAttribute = `data-${this.identifier}-portal-outlet`;
|
|
72
|
+
if (value.length > 0) {
|
|
73
|
+
const controllerAttribute = this.context.schema.controllerAttribute;
|
|
74
|
+
const selector = value.join(', ');
|
|
75
|
+
const portalElements = document.querySelectorAll(selector);
|
|
76
|
+
for (const portalElement of portalElements) {
|
|
77
|
+
if (!portalElement.hasAttribute(controllerAttribute)) {
|
|
78
|
+
portalElement.setAttribute(controllerAttribute, 'portal');
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const existingControllers = portalElement.getAttribute(controllerAttribute)!.split(' ');
|
|
82
|
+
if (!existingControllers.includes('portal')) {
|
|
83
|
+
existingControllers.push('portal');
|
|
84
|
+
portalElement.setAttribute(controllerAttribute, existingControllers.join(' '));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (!this.element.hasAttribute(outletAttribute)) {
|
|
88
|
+
this.element.setAttribute(outletAttribute, selector);
|
|
89
|
+
} else if (this.element.getAttribute(outletAttribute) !== selector) {
|
|
90
|
+
this.element.setAttribute(outletAttribute, selector);
|
|
91
|
+
}
|
|
92
|
+
} else if (this.element.hasAttribute(outletAttribute)) {
|
|
93
|
+
this.element.removeAttribute(outletAttribute);
|
|
94
|
+
}
|
|
95
|
+
if (typeof originalPortalSelectorsValueChanged === 'function') {
|
|
96
|
+
originalPortalSelectorsValueChanged.call(this, value, previousValue);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return derived as unknown as typeof Base & {
|
|
102
|
+
new(...args: any[]): InstanceType<Base> & {
|
|
103
|
+
readonly portalOutlet: Portal;
|
|
104
|
+
readonly hasPortalOutlet: boolean;
|
|
105
|
+
readonly portalOutlets: Portal[];
|
|
106
|
+
portalSelectorsValue: string[];
|
|
107
|
+
readonly hasPortalSelectorsValue: boolean;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import {Application, Controller} from "@hotwired/stimulus";
|
|
2
|
+
import {getController, getControllerAsync} from "./utils";
|
|
3
|
+
|
|
4
|
+
type Constructor<T = {}> = new (...args: any[]) => T;
|
|
5
|
+
|
|
6
|
+
const identifierToAppMap = new Map<string, Application>();
|
|
7
|
+
|
|
8
|
+
export function Resolvable<Base extends Constructor<Controller>>(Base: Base, identifier: string) {
|
|
9
|
+
return class extends Base
|
|
10
|
+
{
|
|
11
|
+
constructor(...args: any[]) {
|
|
12
|
+
super(...args);
|
|
13
|
+
|
|
14
|
+
const originalConnect = (this as any).connect;
|
|
15
|
+
(this as any).connect = function (): void {
|
|
16
|
+
identifierToAppMap.set(this.identifier, this.application);
|
|
17
|
+
if (typeof originalConnect === 'function') {
|
|
18
|
+
originalConnect.call(this);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public static get<T extends Constructor<Controller>>(this: T, element: HTMLElement): InstanceType<T> | null {
|
|
24
|
+
const app = identifierToAppMap.get(identifier);
|
|
25
|
+
if (typeof app === 'undefined') {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
return getController(app, element, identifier) as InstanceType<T> | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public static getAsync<T extends Constructor<Controller>>(
|
|
32
|
+
this: T,
|
|
33
|
+
element: HTMLElement,
|
|
34
|
+
timeout: number = 5000,
|
|
35
|
+
poll: number = 50,
|
|
36
|
+
): Promise<InstanceType<T> | null> {
|
|
37
|
+
const app = identifierToAppMap.get(identifier);
|
|
38
|
+
if (typeof app !== 'undefined') {
|
|
39
|
+
return getControllerAsync(app, element, identifier, timeout, poll) as Promise<InstanceType<T> | null>;
|
|
40
|
+
}
|
|
41
|
+
const startTime = Date.now();
|
|
42
|
+
const maxAttempts = 10;
|
|
43
|
+
let attempts = 0;
|
|
44
|
+
return new Promise((resolve) => {
|
|
45
|
+
const checkApp = async () => {
|
|
46
|
+
attempts++;
|
|
47
|
+
const app = identifierToAppMap.get(identifier);
|
|
48
|
+
if (typeof app !== 'undefined') {
|
|
49
|
+
const remainingTime = timeout - (Date.now() - startTime);
|
|
50
|
+
remainingTime <= 0
|
|
51
|
+
? resolve(getController(app, element, identifier) as InstanceType<T> | null)
|
|
52
|
+
: resolve(await getControllerAsync(app, element, identifier, remainingTime, poll));
|
|
53
|
+
} else if (Date.now() - startTime >= timeout) {
|
|
54
|
+
resolve(null);
|
|
55
|
+
} else if (attempts <= maxAttempts) {
|
|
56
|
+
setTimeout(checkApp);
|
|
57
|
+
} else {
|
|
58
|
+
setTimeout(checkApp, poll);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
checkApp().catch(error => console.error(error));
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
package/src/test.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import {Controller} from '@hotwired/stimulus';
|
|
2
|
+
import {Target, Typed, TypedArray, TypedObject} from './index';
|
|
3
|
+
import {Portals} from "./portal";
|
|
4
|
+
import {Resolvable} from "./resolvable";
|
|
5
|
+
|
|
6
|
+
class OutletController extends Typed(Controller)
|
|
7
|
+
{
|
|
8
|
+
public method(): void {
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
class HomepageController extends Typed(
|
|
13
|
+
Portals(Resolvable(Controller<HTMLElement>, 'homepage')), {
|
|
14
|
+
values: {
|
|
15
|
+
name: String,
|
|
16
|
+
counter: Number,
|
|
17
|
+
isActive: Boolean,
|
|
18
|
+
alias: TypedArray<string>(),
|
|
19
|
+
address: TypedObject<{ street: string }>(),
|
|
20
|
+
metadata: {type: TypedObject<{ title: string }>()},
|
|
21
|
+
},
|
|
22
|
+
targets: {
|
|
23
|
+
form: HTMLFormElement,
|
|
24
|
+
select: HTMLSelectElement,
|
|
25
|
+
custom: Target<{ test: string, someCustomMethod: () => void }>(),
|
|
26
|
+
},
|
|
27
|
+
classes: ['selected', 'highlighted'] as const,
|
|
28
|
+
outlets: {
|
|
29
|
+
'test': OutletController,
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
)
|
|
33
|
+
{
|
|
34
|
+
// All properties are now strongly typed!
|
|
35
|
+
|
|
36
|
+
public connect(): void {
|
|
37
|
+
// String values
|
|
38
|
+
this.nameValue.split(' ');
|
|
39
|
+
|
|
40
|
+
// Number values
|
|
41
|
+
Math.floor(this.counterValue);
|
|
42
|
+
|
|
43
|
+
// Boolean values
|
|
44
|
+
this.isActiveValue;
|
|
45
|
+
|
|
46
|
+
// Array values
|
|
47
|
+
this.aliasValue.map(alias => alias.toUpperCase());
|
|
48
|
+
|
|
49
|
+
// Object values
|
|
50
|
+
console.log(this.addressValue.street);
|
|
51
|
+
|
|
52
|
+
console.log(this.metadataValue.title);
|
|
53
|
+
|
|
54
|
+
// Targets
|
|
55
|
+
this.formTarget.submit();
|
|
56
|
+
this.selectTarget.value = 'stimulus';
|
|
57
|
+
this.customTarget.someCustomMethod();
|
|
58
|
+
|
|
59
|
+
// Outlets
|
|
60
|
+
this.testOutlet.method();
|
|
61
|
+
|
|
62
|
+
// Classes
|
|
63
|
+
if (this.hasSelectedClass) {
|
|
64
|
+
console.log(this.selectedClass);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Portals
|
|
68
|
+
if (this.hasPortalOutlet) {
|
|
69
|
+
console.log(this.portalOutlet);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
package/src/typed.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import {Controller} from "@hotwired/stimulus";
|
|
2
|
+
|
|
3
|
+
class Wrapped<T = any>
|
|
4
|
+
{
|
|
5
|
+
private _: T | undefined = undefined;
|
|
6
|
+
|
|
7
|
+
public constructor(
|
|
8
|
+
public readonly context: 'typed-object' | 'typed-array' | 'target',
|
|
9
|
+
) {
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const TypedObject = <T extends object>() => new Wrapped<T>('typed-object');
|
|
14
|
+
|
|
15
|
+
export const TypedArray = <T>() => new Wrapped<T[]>('typed-array');
|
|
16
|
+
|
|
17
|
+
export const Target = <T extends object>() => new Wrapped<T>('target');
|
|
18
|
+
|
|
19
|
+
type Constructor<T = {}> = new (...args: any[]) => T;
|
|
20
|
+
|
|
21
|
+
type CamelCase<K extends string> =
|
|
22
|
+
K extends `${infer T}_${infer U}`
|
|
23
|
+
? `${Uncapitalize<T>}${Capitalize<CamelCase<U>>}`
|
|
24
|
+
: K extends `${infer T}-${infer U}`
|
|
25
|
+
? `${Uncapitalize<T>}${Capitalize<CamelCase<U>>}`
|
|
26
|
+
: K extends `${infer T} ${infer U}`
|
|
27
|
+
? `${Uncapitalize<T>}${Capitalize<CamelCase<U>>}`
|
|
28
|
+
: K;
|
|
29
|
+
|
|
30
|
+
type ClassProperties<Classes extends readonly string[] = []> =
|
|
31
|
+
{ [K in Classes[number] as `${CamelCase<K>}Class`]: string } &
|
|
32
|
+
{ readonly [K in Classes[number] as `has${Capitalize<CamelCase<K>>}Class`]: boolean } &
|
|
33
|
+
{ [K in Classes[number] as `${CamelCase<K>}Classes`]: string[] };
|
|
34
|
+
|
|
35
|
+
type ValueTypeDefault = string | number | boolean | Array<any> | Object | InstanceType<typeof Wrapped>;
|
|
36
|
+
|
|
37
|
+
type ValueTypeConstant =
|
|
38
|
+
| typeof String
|
|
39
|
+
| typeof Number
|
|
40
|
+
| typeof Boolean
|
|
41
|
+
| typeof Array<any>
|
|
42
|
+
| typeof Object
|
|
43
|
+
| InstanceType<typeof Wrapped>
|
|
44
|
+
|
|
45
|
+
type ValueTypeObject = {
|
|
46
|
+
type: ValueTypeConstant;
|
|
47
|
+
default?: ValueTypeDefault;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
type ValueTypeDefinition = ValueTypeConstant | ValueTypeObject | InstanceType<typeof Wrapped>;
|
|
51
|
+
|
|
52
|
+
type ValueDefinitionMap = {
|
|
53
|
+
[token: string]: ValueTypeDefinition;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
type TypeFromConstructor<C> =
|
|
57
|
+
C extends StringConstructor
|
|
58
|
+
? string
|
|
59
|
+
: C extends NumberConstructor
|
|
60
|
+
? number
|
|
61
|
+
: C extends BooleanConstructor
|
|
62
|
+
? boolean
|
|
63
|
+
: C extends ArrayConstructor
|
|
64
|
+
? any[]
|
|
65
|
+
: C extends Wrapped<infer T>
|
|
66
|
+
? T
|
|
67
|
+
: C extends ObjectConstructor
|
|
68
|
+
? Object
|
|
69
|
+
: C extends Constructor<infer T>
|
|
70
|
+
? TypeFromConstructor<T>
|
|
71
|
+
: never;
|
|
72
|
+
|
|
73
|
+
type TransformValueDefinition<T extends ValueTypeDefinition> =
|
|
74
|
+
T extends { type: infer U }
|
|
75
|
+
? TypeFromConstructor<U>
|
|
76
|
+
: TypeFromConstructor<T>;
|
|
77
|
+
|
|
78
|
+
type ValuesProperties<Values extends ValueDefinitionMap> =
|
|
79
|
+
{ [K in keyof Values as `${CamelCase<K & string>}Value`]: TransformValueDefinition<Values[K]> } &
|
|
80
|
+
{ readonly [K in keyof Values as `has${Capitalize<CamelCase<K & string>>}Value`]: boolean };
|
|
81
|
+
|
|
82
|
+
type TargetTypeDefinition = typeof Element | InstanceType<typeof Wrapped>;
|
|
83
|
+
|
|
84
|
+
type TargetsDefinitionMap = {
|
|
85
|
+
[token: string]: TargetTypeDefinition;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
type TransformTargetDefinition<T extends TargetTypeDefinition> =
|
|
89
|
+
T extends Wrapped<infer U>
|
|
90
|
+
? U
|
|
91
|
+
: T extends new (...args: any[]) => infer R
|
|
92
|
+
? R
|
|
93
|
+
: never;
|
|
94
|
+
|
|
95
|
+
type TargetsProperties<Targets extends TargetsDefinitionMap> =
|
|
96
|
+
{ readonly [K in keyof Targets as `${CamelCase<K & string>}Target`]: TransformTargetDefinition<Targets[K]> } &
|
|
97
|
+
{ readonly [K in keyof Targets as `has${Capitalize<CamelCase<K & string>>}Target`]: boolean } &
|
|
98
|
+
{ readonly [K in keyof Targets as `${CamelCase<K & string>}Targets`]: TransformTargetDefinition<Targets[K]>[] };
|
|
99
|
+
|
|
100
|
+
type OutletsDefinitionMap = {
|
|
101
|
+
[token: string]: Constructor<Controller>;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
type OutletProperties<Outlets extends OutletsDefinitionMap> =
|
|
105
|
+
{ readonly [K in keyof Outlets as `${CamelCase<K & string>}Outlet`]: InstanceType<Outlets[K]> } &
|
|
106
|
+
{ readonly [K in keyof Outlets as `has${Capitalize<CamelCase<K & string>>}Outlet`]: boolean } &
|
|
107
|
+
{ readonly [K in keyof Outlets as `${CamelCase<K & string>}Outlets`]: InstanceType<Outlets[K]>[] };
|
|
108
|
+
|
|
109
|
+
type Configuration<
|
|
110
|
+
Values extends ValueDefinitionMap,
|
|
111
|
+
Targets extends TargetsDefinitionMap,
|
|
112
|
+
Classes extends readonly string[],
|
|
113
|
+
Outlets extends OutletsDefinitionMap,
|
|
114
|
+
> = {
|
|
115
|
+
values?: Values;
|
|
116
|
+
targets?: Targets;
|
|
117
|
+
classes?: Classes;
|
|
118
|
+
outlets?: Outlets;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
function patchValueTypeDefinitionMap(values: ValueDefinitionMap): ValueDefinitionMap {
|
|
122
|
+
const patchedValues: ValueDefinitionMap = {};
|
|
123
|
+
const patchType = (type: any) => {
|
|
124
|
+
if (type instanceof Wrapped && type.context === 'typed-object') {
|
|
125
|
+
return Object;
|
|
126
|
+
}
|
|
127
|
+
if (type instanceof Wrapped && type.context === 'typed-array') {
|
|
128
|
+
return Array;
|
|
129
|
+
}
|
|
130
|
+
return type;
|
|
131
|
+
};
|
|
132
|
+
Object.getOwnPropertyNames(values).forEach(key => {
|
|
133
|
+
const definition = values[key];
|
|
134
|
+
if (typeof definition === 'object' && 'default' in definition && 'type' in definition) {
|
|
135
|
+
patchedValues[key] = {
|
|
136
|
+
type: patchType(definition.type),
|
|
137
|
+
default: definition.default,
|
|
138
|
+
};
|
|
139
|
+
} else if (typeof definition === 'object' && 'type' in definition) {
|
|
140
|
+
patchedValues[key] = patchType(definition.type);
|
|
141
|
+
} else if (definition instanceof Wrapped) {
|
|
142
|
+
patchedValues[key] = patchType(definition);
|
|
143
|
+
} else {
|
|
144
|
+
patchedValues[key] = definition;
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
return patchedValues;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function Typed<
|
|
151
|
+
Base extends Constructor<Controller>,
|
|
152
|
+
Values extends ValueDefinitionMap = {},
|
|
153
|
+
Targets extends TargetsDefinitionMap = {},
|
|
154
|
+
Classes extends readonly string[] = [],
|
|
155
|
+
Outlets extends OutletsDefinitionMap = {},
|
|
156
|
+
>(Base: Base, configuration?: Configuration<Values, Targets, Classes, Outlets>) {
|
|
157
|
+
const {values, targets, classes, outlets} = configuration ?? {};
|
|
158
|
+
|
|
159
|
+
const derived = class extends Base
|
|
160
|
+
{
|
|
161
|
+
constructor(...args: any[]) {
|
|
162
|
+
super(...args);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
static values = patchValueTypeDefinitionMap(values ?? {});
|
|
166
|
+
static targets = Object.getOwnPropertyNames(targets ?? {});
|
|
167
|
+
static classes = classes ?? [];
|
|
168
|
+
static outlets = Object.getOwnPropertyNames(outlets ?? {});
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
return derived as unknown as typeof Base & {
|
|
172
|
+
new(...args: any[]): InstanceType<Base>
|
|
173
|
+
& ValuesProperties<Values>
|
|
174
|
+
& TargetsProperties<Targets>
|
|
175
|
+
& ClassProperties<Classes>
|
|
176
|
+
& OutletProperties<Outlets>;
|
|
177
|
+
}
|
|
178
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import {ActionEvent, Application, Controller} from "@hotwired/stimulus";
|
|
2
|
+
|
|
3
|
+
export const camelCase = (value: string): string => {
|
|
4
|
+
return value
|
|
5
|
+
.replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ''))
|
|
6
|
+
.replace(/^[A-Z]/, c => c.toLowerCase());
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const capitalize = (value: string): string => {
|
|
10
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const isActionEvent = (value: any): value is ActionEvent => {
|
|
14
|
+
return value instanceof Event && 'params' in value && typeof value.params !== 'object';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const getController = <T extends Controller>(app: Application, element: HTMLElement, identifier: string): T | null => {
|
|
18
|
+
return app.getControllerForElementAndIdentifier(element, identifier) as T | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const getControllerAsync = async <T extends Controller>(app: Application, element: HTMLElement, identifier: string, timeout: number = 5000, poll: number = 50): Promise<T | null> => {
|
|
22
|
+
const startTime = Date.now();
|
|
23
|
+
const maxAttempts = 10;
|
|
24
|
+
let attempts = 0;
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
const checkController = () => {
|
|
27
|
+
attempts++;
|
|
28
|
+
const controller = app.getControllerForElementAndIdentifier(element, identifier) as T | null;
|
|
29
|
+
if (controller !== null) {
|
|
30
|
+
resolve(controller);
|
|
31
|
+
} else if (Date.now() - startTime >= timeout) {
|
|
32
|
+
resolve(null);
|
|
33
|
+
} else if (attempts <= maxAttempts) {
|
|
34
|
+
setTimeout(checkController);
|
|
35
|
+
} else {
|
|
36
|
+
setTimeout(checkController, poll);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
checkController();
|
|
40
|
+
});
|
|
41
|
+
};
|