@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/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Jan Pech
|
|
4
|
+
|
|
5
|
+
This project is based on:
|
|
6
|
+
- stimulus-typescript (https://github.com/ajaishankar/stimulus-typescript) - Copyright (c) Ajai Shankar
|
|
7
|
+
- headless-components-rails (https://github.com/Tonksthebear/headless-components-rails) - Copyright (c) Tonksthebear
|
|
8
|
+
|
|
9
|
+
This project includes code from the above-mentioned projects, each licensed under the MIT License.
|
|
10
|
+
Their respective license terms apply to their portions of the code.
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# Stimulus TypeScript
|
|
2
|
+
|
|
3
|
+
This project is based on the following projects:
|
|
4
|
+
|
|
5
|
+
- [stimulus-typescript](https://github.com/ajaishankar/stimulus-typescript/tree/main) by Ajai Shankar
|
|
6
|
+
- [headless-components-rails](https://github.com/Tonksthebear/headless-components-rails) by Tonksthebear
|
|
7
|
+
|
|
8
|
+
I would like to thank the authors of these projects for their work, which served as the foundation for this package.
|
|
9
|
+
|
|
10
|
+
## MIT Licenses of Original Projects
|
|
11
|
+
|
|
12
|
+
- [stimulus-typescript MIT License](https://github.com/ajaishankar/stimulus-typescript/tree/main?tab=MIT-1-ov-file)
|
|
13
|
+
- [headless-components-rails MIT License](https://github.com/Tonksthebear/headless-components-rails?tab=MIT-1-ov-file)
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
This package provides strongly typed Stimulus controllers with TypeScript, offering type safety for values, targets, classes, outlets, and portals.
|
|
18
|
+
|
|
19
|
+
### Basic Usage
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import {Controller} from '@hotwired/stimulus';
|
|
23
|
+
import {Target, Typed, TypedArray, TypedObject} from '@pechynho/stimulus-typescript';
|
|
24
|
+
import {UserStatusController} from './user-status-controller';
|
|
25
|
+
import {CustomElement} from './custom-element';
|
|
26
|
+
|
|
27
|
+
class HomepageController extends Typed(
|
|
28
|
+
Controller<HTMLElement>, {
|
|
29
|
+
values: {
|
|
30
|
+
name: String,
|
|
31
|
+
counter: Number,
|
|
32
|
+
isActive: Boolean,
|
|
33
|
+
alias: TypedArray<string>(),
|
|
34
|
+
address: TypedObject<{ street: string }>(),
|
|
35
|
+
},
|
|
36
|
+
targets: {
|
|
37
|
+
form: HTMLFormElement,
|
|
38
|
+
select: HTMLSelectElement,
|
|
39
|
+
custom: Target<CustomElement>(),
|
|
40
|
+
},
|
|
41
|
+
classes: ['selected', 'highlighted'] as const,
|
|
42
|
+
outlets: {'user-status': UserStatusController},
|
|
43
|
+
}
|
|
44
|
+
)
|
|
45
|
+
{
|
|
46
|
+
// All properties are now strongly typed!
|
|
47
|
+
|
|
48
|
+
public connect(): void {
|
|
49
|
+
// String values
|
|
50
|
+
this.nameValue.split(' ');
|
|
51
|
+
|
|
52
|
+
// Number values
|
|
53
|
+
Math.floor(this.counterValue);
|
|
54
|
+
|
|
55
|
+
// Boolean values
|
|
56
|
+
this.isActiveValue;
|
|
57
|
+
|
|
58
|
+
// Array values
|
|
59
|
+
this.aliasValue.map(alias => alias.toUpperCase());
|
|
60
|
+
|
|
61
|
+
// Object values
|
|
62
|
+
console.log(this.addressValue.street);
|
|
63
|
+
|
|
64
|
+
// Targets
|
|
65
|
+
this.formTarget.submit();
|
|
66
|
+
this.selectTarget.value = 'stimulus';
|
|
67
|
+
this.customTarget.someCustomMethod();
|
|
68
|
+
|
|
69
|
+
// Outlets
|
|
70
|
+
this.userStatusOutlets.forEach(status => status.markAsSelected(event));
|
|
71
|
+
|
|
72
|
+
// Classes
|
|
73
|
+
if (this.hasSelectedClass) {
|
|
74
|
+
console.log(this.selectedClass);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Type Definitions
|
|
81
|
+
|
|
82
|
+
#### Values
|
|
83
|
+
|
|
84
|
+
The `values` object defines the types of values that can be set on your controller:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
import {TypedArray, TypedObject} from "./typed-stimulus";
|
|
88
|
+
|
|
89
|
+
const values = {
|
|
90
|
+
// Basic types
|
|
91
|
+
name: String, // string
|
|
92
|
+
count: Number, // number
|
|
93
|
+
isActive: Boolean, // boolean
|
|
94
|
+
|
|
95
|
+
// Array types
|
|
96
|
+
tags: TypedArray<string>(), // string[]
|
|
97
|
+
scores: TypedArray<number>(), // number[]
|
|
98
|
+
|
|
99
|
+
// Custom object type
|
|
100
|
+
user: TypedObject<{
|
|
101
|
+
firstName: string,
|
|
102
|
+
lastName: string,
|
|
103
|
+
age: number
|
|
104
|
+
}>()
|
|
105
|
+
};
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
#### Targets
|
|
109
|
+
|
|
110
|
+
The `targets` object defines the HTML elements that your controller can target:
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
import {Target} from '@pechynho/stimulus-typescript';
|
|
114
|
+
import {CustomElement} from './custom-element';
|
|
115
|
+
|
|
116
|
+
const targets = {
|
|
117
|
+
form: HTMLFormElement, // <div data-homepage-controller-target="form"></div>
|
|
118
|
+
button: HTMLButtonElement, // <button data-homepage-controller-targe="bubton"></button>
|
|
119
|
+
input: HTMLInputElement, // <input data-homepage-controller-target="input">
|
|
120
|
+
custom: Target<CustomElement>(), // <div data-homepage-controller-target="custom"></div>
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
#### Classes
|
|
125
|
+
|
|
126
|
+
The `classes` array defines CSS classes that your controller can add/remove:
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
const classes = ['selected', 'highlighted', 'active'] as const;
|
|
130
|
+
|
|
131
|
+
// Usage:
|
|
132
|
+
this.hasSelectedClass // boolean
|
|
133
|
+
this.selectedClass // string (class name)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
#### Outlets
|
|
137
|
+
|
|
138
|
+
The `outlets` object defines other controllers that your controller can communicate with:
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
import {UserStatusController} from './user-status-controller';
|
|
142
|
+
import {NotificationController} from './notification-controller';
|
|
143
|
+
|
|
144
|
+
const outlets = {
|
|
145
|
+
'user-status': UserStatusController,
|
|
146
|
+
'notification': NotificationController
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Usage:
|
|
150
|
+
this.hasUserStatusOutlet // boolean
|
|
151
|
+
this.userStatusOutlet // UserStatusController
|
|
152
|
+
this.userStatusOutlets // UserStatusController[]
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Portals
|
|
156
|
+
|
|
157
|
+
When you define portals in your controller, the system:
|
|
158
|
+
|
|
159
|
+
1. Monitors these elements for targets and actions
|
|
160
|
+
2. Makes these targets available to your controller
|
|
161
|
+
3. Routes actions from these elements to your controller
|
|
162
|
+
|
|
163
|
+
This is especially useful for modals, sidebars, or any other elements that might be rendered outside your controller's DOM tree but still need to interact with your controller.
|
|
164
|
+
|
|
165
|
+
You need to register special `PortalController` to your Stimulus application:
|
|
166
|
+
```typescript
|
|
167
|
+
import { Application } from '@hotwired/stimulus';
|
|
168
|
+
import { PortalController } from '@pechynho/stimulus-typescript';
|
|
169
|
+
|
|
170
|
+
const app = Application.start(); // Start your Stimulus application
|
|
171
|
+
|
|
172
|
+
app.register('portal', PortalController); // Register PortalController
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
#### Example
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
import { Controller } from '@hotwired/stimulus';
|
|
179
|
+
import { Typed, Portals } from '@pechynho/stimulus-typescript';
|
|
180
|
+
|
|
181
|
+
class ModalController extends Typed(
|
|
182
|
+
Portals(Controller<HTMLElement>), {
|
|
183
|
+
targets: {
|
|
184
|
+
content: HTMLDivElement
|
|
185
|
+
},
|
|
186
|
+
}
|
|
187
|
+
) {
|
|
188
|
+
public open(): void {
|
|
189
|
+
// Even if #modal is outside this controller's DOM,
|
|
190
|
+
// you can still access targets inside it
|
|
191
|
+
this.contentTarget.classList.add('visible');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
public close(): void {
|
|
195
|
+
this.contentTarget.classList.remove('visible');
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
In your HTML:
|
|
201
|
+
|
|
202
|
+
```html
|
|
203
|
+
<div data-controller="modal" data-modal-portal-selectors-value="[#modal]">
|
|
204
|
+
<button data-action="modal#open">Open Modal</button>
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
<!-- This is outside the controller's DOM -->
|
|
208
|
+
<div id="modal">
|
|
209
|
+
<div data-modal-target="content">
|
|
210
|
+
Modal content here
|
|
211
|
+
<button data-action="modal#close">Close</button>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
With portals, the ModalController can interact with elements inside #modal even though they're outside its DOM hierarchy.
|
|
217
|
+
|
|
218
|
+
### Resolvable
|
|
219
|
+
|
|
220
|
+
When you use the Resolvable feature, your controller class gains two static methods:
|
|
221
|
+
|
|
222
|
+
1. `get<T>`: Synchronously gets a controller instance for a specific element
|
|
223
|
+
2. `getAsync<T>`: Asynchronously gets a controller instance with timeout and polling options
|
|
224
|
+
|
|
225
|
+
#### Example
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
import { Controller } from '@hotwired/stimulus';
|
|
229
|
+
import { Typed, Resolvable } from '@pechynho/stimulus-typescript';
|
|
230
|
+
|
|
231
|
+
class UserController extends Typed(
|
|
232
|
+
Resolvable(Controller<HTMLElement>, 'user'), {
|
|
233
|
+
values: {
|
|
234
|
+
name: String,
|
|
235
|
+
},
|
|
236
|
+
}
|
|
237
|
+
) {
|
|
238
|
+
public greet(): void {
|
|
239
|
+
console.log(`Hello, ${this.nameValue}!`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Later, in another part of your code:
|
|
244
|
+
const userElement = document.querySelector('#user');
|
|
245
|
+
|
|
246
|
+
// Synchronous access (returns null if controller is not found)
|
|
247
|
+
const userController = UserController.get(userElement);
|
|
248
|
+
if (userController) {
|
|
249
|
+
userController.greet();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Asynchronous access (resolves when controller is found or rejects after timeout)
|
|
253
|
+
UserController.getAsync(userElement)
|
|
254
|
+
.then(controller => {
|
|
255
|
+
if (controller !== null) {
|
|
256
|
+
controller.greet();
|
|
257
|
+
}
|
|
258
|
+
})
|
|
259
|
+
.catch(error => console.error(error));
|
|
260
|
+
|
|
261
|
+
// With custom timeout and polling interval (in milliseconds)
|
|
262
|
+
UserController.getAsync(userElement, 10000, 100)
|
|
263
|
+
.then(controller => {
|
|
264
|
+
if (controller !== nu) {
|
|
265
|
+
controller.greet();
|
|
266
|
+
}
|
|
267
|
+
})
|
|
268
|
+
.catch(error => console.error(error));
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
This is particularly useful when:
|
|
272
|
+
- Working with dynamically loaded content
|
|
273
|
+
- Integrating with non-Stimulus JavaScript libraries
|
|
274
|
+
- Communicating between controllers that don't have a parent-child relationship
|
|
275
|
+
- You've just added an element to the DOM and want it to resolve to a controller, so you use `getAsync` and you do not have to deal with Stimulus internal timing (has Stimulus already discovered a new element and connected controller?)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { default as PortalController } from './portal-controller';
|
|
2
|
+
export { Typed, Target, TypedObject, TypedArray } from './typed';
|
|
3
|
+
export { isActionEvent, getController, getControllerAsync } from './utils';
|
|
4
|
+
export { Resolvable } from './resolvable';
|
|
5
|
+
export { Portals } from './portal';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Export the main components
|
|
2
|
+
export { default as PortalController } from './portal-controller';
|
|
3
|
+
export { Typed, Target, TypedObject, TypedArray } from './typed';
|
|
4
|
+
export { isActionEvent, getController, getControllerAsync } from './utils';
|
|
5
|
+
export { Resolvable } from './resolvable';
|
|
6
|
+
export { Portals } from './portal';
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
export default class extends Controller<HTMLElement> {
|
|
3
|
+
private observer;
|
|
4
|
+
private isConnected;
|
|
5
|
+
private identifiers;
|
|
6
|
+
private searchedIdentifiersForTargets;
|
|
7
|
+
private searchedIdentifiersForActions;
|
|
8
|
+
private controllers;
|
|
9
|
+
private targetsByController;
|
|
10
|
+
private targetsByIdentifier;
|
|
11
|
+
private targetsByTargetName;
|
|
12
|
+
private controllerOriginalMethods;
|
|
13
|
+
private actionToElementsMap;
|
|
14
|
+
private elementToActionsMap;
|
|
15
|
+
private identifierToActionElementsMap;
|
|
16
|
+
private actionElementToIdentifiersMap;
|
|
17
|
+
initialize(): void;
|
|
18
|
+
connect(): void;
|
|
19
|
+
disconnect(): void;
|
|
20
|
+
sync(controller: Controller): void;
|
|
21
|
+
unsync(controller: Controller): void;
|
|
22
|
+
private reinitializeObserver;
|
|
23
|
+
private connectObserver;
|
|
24
|
+
private disconnectObserver;
|
|
25
|
+
private handleMutations;
|
|
26
|
+
private addTarget;
|
|
27
|
+
private removeTarget;
|
|
28
|
+
private disconnectAllTargets;
|
|
29
|
+
private searchTargets;
|
|
30
|
+
private searchActions;
|
|
31
|
+
private getTargetConnectedMethodName;
|
|
32
|
+
private getTargetDisconnectedMethodName;
|
|
33
|
+
private getTargetAttributeName;
|
|
34
|
+
private getActionAttributeName;
|
|
35
|
+
private getPortalledActionAttributeName;
|
|
36
|
+
private isObservedTargetElement;
|
|
37
|
+
private overrideControllerGetTargetMethods;
|
|
38
|
+
private restoreControllerGetTargetMethods;
|
|
39
|
+
private restoreControllersGetTargetMethods;
|
|
40
|
+
private storeTargetByTargetName;
|
|
41
|
+
private removeStoredTargetByTargetName;
|
|
42
|
+
private hasStoredTargetsByTargetName;
|
|
43
|
+
private getStoredTargetsByTargetName;
|
|
44
|
+
private addActionElement;
|
|
45
|
+
private removeActionElement;
|
|
46
|
+
private removeAllProxyActions;
|
|
47
|
+
private removeAllProxyActionsByIdentifier;
|
|
48
|
+
private parseActions;
|
|
49
|
+
private parseActionToken;
|
|
50
|
+
private getProxyActionName;
|
|
51
|
+
private getActionParams;
|
|
52
|
+
private toProxyAction;
|
|
53
|
+
private setAttributeValue;
|
|
54
|
+
}
|