@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 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?)
@@ -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
+ }