application.ts 1.0.0
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 +21 -0
- package/README.md +358 -0
- package/dist/app/App.d.ts +124 -0
- package/dist/app/App.js +326 -0
- package/dist/app/App.js.map +1 -0
- package/dist/app/AppView.d.ts +143 -0
- package/dist/app/AppView.js +264 -0
- package/dist/app/AppView.js.map +1 -0
- package/dist/app/types.d.ts +67 -0
- package/dist/app/types.js +2 -0
- package/dist/app/types.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/navigation/route.d.ts +34 -0
- package/dist/navigation/route.js +68 -0
- package/dist/navigation/route.js.map +1 -0
- package/dist/navigation/router.d.ts +92 -0
- package/dist/navigation/router.js +246 -0
- package/dist/navigation/router.js.map +1 -0
- package/dist/navigation/types.d.ts +63 -0
- package/dist/navigation/types.js +9 -0
- package/dist/navigation/types.js.map +1 -0
- package/package.json +68 -0
- package/src/app/App.ts +391 -0
- package/src/app/AppView.ts +333 -0
- package/src/app/types.ts +78 -0
- package/src/index.ts +28 -0
- package/src/navigation/route.ts +87 -0
- package/src/navigation/router.ts +284 -0
- package/src/navigation/types.ts +73 -0
package/src/app/App.ts
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import StackView from 'stackview.ts';
|
|
2
|
+
import { Router } from '../navigation/router';
|
|
3
|
+
import { NavigationEvents, type NavigationEventDetail } from '../navigation/types';
|
|
4
|
+
import type { AppView } from './AppView';
|
|
5
|
+
|
|
6
|
+
// Force StackView to be included in the bundle by using it
|
|
7
|
+
// This ensures the custom element is registered when the module loads
|
|
8
|
+
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
10
|
+
const _stackView = StackView;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* View constructor type
|
|
14
|
+
*/
|
|
15
|
+
type ViewConstructor = new () => AppView<any>;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Main App class that manages routing and view rendering
|
|
19
|
+
* Integrates Router with StackView.Ts for navigation
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* import { App } from 'application.ts';
|
|
24
|
+
* import { HomeView, AboutView, NotFoundView } from './views';
|
|
25
|
+
*
|
|
26
|
+
* const app = new App('#root');
|
|
27
|
+
*
|
|
28
|
+
* app.router
|
|
29
|
+
* .map('/', HomeView)
|
|
30
|
+
* .map('/about', AboutView)
|
|
31
|
+
* .notFound(NotFoundView);
|
|
32
|
+
*
|
|
33
|
+
* app.start();
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export class App {
|
|
37
|
+
public readonly router: Router;
|
|
38
|
+
private stackView: any = null;
|
|
39
|
+
private rootElement: HTMLElement | null = null;
|
|
40
|
+
private viewRegistry: Map<string, ViewConstructor> = new Map();
|
|
41
|
+
private layoutRegistry: Map<string, ViewConstructor> = new Map();
|
|
42
|
+
private currentViewInstance: AppView<any> | null = null;
|
|
43
|
+
private currentLayoutInstance: AppView<any> | null = null;
|
|
44
|
+
private currentRoutePath: string | null = null;
|
|
45
|
+
private defaultLayout: string | null = null;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get the App instance from any element by traversing up the DOM tree
|
|
49
|
+
* @param element - Any HTMLElement in the app
|
|
50
|
+
* @returns App instance or null if not found
|
|
51
|
+
*/
|
|
52
|
+
static fromElement(element: HTMLElement | null): App | null {
|
|
53
|
+
let current = element;
|
|
54
|
+
|
|
55
|
+
while (current) {
|
|
56
|
+
if ((current as any).__app__) {
|
|
57
|
+
return (current as any).__app__;
|
|
58
|
+
}
|
|
59
|
+
current = current.parentElement;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
constructor(rootSelector?: string) {
|
|
66
|
+
this.router = new Router();
|
|
67
|
+
|
|
68
|
+
// Listen to navigation events
|
|
69
|
+
this.router.addEventListener(NavigationEvents.NAVIGATE, ((e: CustomEvent<NavigationEventDetail>) => {
|
|
70
|
+
this.handleNavigation(e);
|
|
71
|
+
}) as EventListener);
|
|
72
|
+
|
|
73
|
+
this.router.addEventListener(NavigationEvents.NOT_FOUND, ((e: CustomEvent<NavigationEventDetail>) => {
|
|
74
|
+
this.handleNotFound(e);
|
|
75
|
+
}) as EventListener);
|
|
76
|
+
|
|
77
|
+
// If root selector provided, initialize immediately
|
|
78
|
+
if (rootSelector) {
|
|
79
|
+
this.initializeRoot(rootSelector);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Register a view class with a handler name
|
|
85
|
+
* @param handler - The view handler/identifier used in router.map()
|
|
86
|
+
* @param viewClass - The AppView class constructor
|
|
87
|
+
*/
|
|
88
|
+
registerView(handler: string, viewClass: ViewConstructor): this {
|
|
89
|
+
// Auto-register the custom element
|
|
90
|
+
(viewClass as any).register();
|
|
91
|
+
this.viewRegistry.set(handler, viewClass);
|
|
92
|
+
return this;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Register multiple views at once
|
|
97
|
+
* @param views - Object mapping handler names to view classes
|
|
98
|
+
*/
|
|
99
|
+
registerViews(views: Record<string, ViewConstructor>): this {
|
|
100
|
+
for (const [handler, viewClass] of Object.entries(views)) {
|
|
101
|
+
this.registerView(handler, viewClass);
|
|
102
|
+
}
|
|
103
|
+
return this;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Register a layout class with a handler name
|
|
108
|
+
* @param handler - The layout handler/identifier
|
|
109
|
+
* @param layoutClass - The AppView layout class constructor
|
|
110
|
+
*/
|
|
111
|
+
registerLayout(handler: string, layoutClass: ViewConstructor): this {
|
|
112
|
+
// Auto-register the custom element
|
|
113
|
+
(layoutClass as any).register();
|
|
114
|
+
this.layoutRegistry.set(handler, layoutClass);
|
|
115
|
+
return this;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Register multiple layouts at once
|
|
120
|
+
* @param layouts - Object mapping handler names to layout classes
|
|
121
|
+
*/
|
|
122
|
+
registerLayouts(layouts: Record<string, ViewConstructor>): this {
|
|
123
|
+
for (const [handler, layoutClass] of Object.entries(layouts)) {
|
|
124
|
+
this.registerLayout(handler, layoutClass);
|
|
125
|
+
}
|
|
126
|
+
return this;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Set the default layout for all routes
|
|
131
|
+
* @param handler - The layout handler name, or null for no layout
|
|
132
|
+
*/
|
|
133
|
+
setDefaultLayout(handler: string | null): this {
|
|
134
|
+
this.defaultLayout = handler;
|
|
135
|
+
return this;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Initialize the root element and StackView
|
|
140
|
+
* @param selector - CSS selector for the root element
|
|
141
|
+
*/
|
|
142
|
+
private initializeRoot(selector: string): void {
|
|
143
|
+
const element = document.querySelector(selector);
|
|
144
|
+
if (!element) {
|
|
145
|
+
throw new Error(`Root element not found: ${selector}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
this.rootElement = element as HTMLElement;
|
|
149
|
+
|
|
150
|
+
// Attach app instance to root element
|
|
151
|
+
(this.rootElement as any).__app__ = this;
|
|
152
|
+
|
|
153
|
+
// Create StackView if it doesn't exist
|
|
154
|
+
if (!this.stackView) {
|
|
155
|
+
// Create or use existing stack-view element
|
|
156
|
+
let stackViewElement = this.rootElement.querySelector('stack-view') as StackView;
|
|
157
|
+
|
|
158
|
+
if (!stackViewElement) {
|
|
159
|
+
stackViewElement = new StackView();
|
|
160
|
+
stackViewElement.effect = 'fade';
|
|
161
|
+
stackViewElement.backButton = false;
|
|
162
|
+
this.rootElement.appendChild(stackViewElement);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
this.stackView = stackViewElement;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Start the application
|
|
171
|
+
* @param rootSelector - Optional root selector if not provided in constructor
|
|
172
|
+
*/
|
|
173
|
+
start(rootSelector?: string): void {
|
|
174
|
+
if (rootSelector) {
|
|
175
|
+
this.initializeRoot(rootSelector);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (!this.rootElement || !this.stackView) {
|
|
179
|
+
throw new Error('Root element not initialized. Provide selector in constructor or start()');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Start the router
|
|
183
|
+
this.router.start();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Resolve view class from handler (supports both class constructor and string lookup)
|
|
188
|
+
* @param handler - View class constructor or string identifier
|
|
189
|
+
* @returns View class constructor or null
|
|
190
|
+
*/
|
|
191
|
+
private resolveViewClass(handler: any): ViewConstructor | null {
|
|
192
|
+
// If handler is already a class constructor, use it directly
|
|
193
|
+
if (typeof handler === 'function') {
|
|
194
|
+
// Auto-register the custom element
|
|
195
|
+
(handler as any).register?.();
|
|
196
|
+
return handler as ViewConstructor;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// If handler is a string, look it up in the registry (backward compatibility)
|
|
200
|
+
if (typeof handler === 'string') {
|
|
201
|
+
return this.viewRegistry.get(handler) || null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Handle navigation event from router
|
|
209
|
+
*/
|
|
210
|
+
private async handleNavigation(event: CustomEvent<NavigationEventDetail>): Promise<void> {
|
|
211
|
+
const { handler, params, meta, path } = event.detail;
|
|
212
|
+
|
|
213
|
+
// Store the current route path pattern
|
|
214
|
+
this.currentRoutePath = this.getRoutePattern(path);
|
|
215
|
+
|
|
216
|
+
// Resolve view class from handler (can be class constructor or string)
|
|
217
|
+
const ViewClass = this.resolveViewClass(handler);
|
|
218
|
+
|
|
219
|
+
if (!ViewClass) {
|
|
220
|
+
console.error(`View not found or registered: ${handler}`);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Check if we're navigating to the same view instance with different params
|
|
225
|
+
if (this.currentViewInstance && this.currentViewInstance.constructor === ViewClass) {
|
|
226
|
+
// Same view, just update parameters
|
|
227
|
+
await this.currentViewInstance.updateParams(params);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Determine which layout to use
|
|
232
|
+
const layoutHandler = meta?.layout !== undefined ? meta.layout : this.defaultLayout;
|
|
233
|
+
|
|
234
|
+
// Create view instance
|
|
235
|
+
const viewInstance = new ViewClass();
|
|
236
|
+
|
|
237
|
+
// Store current view instance
|
|
238
|
+
this.currentViewInstance = viewInstance;
|
|
239
|
+
|
|
240
|
+
// If layout is specified, wrap view in layout
|
|
241
|
+
if (layoutHandler) {
|
|
242
|
+
await this.renderWithLayout(viewInstance, layoutHandler, meta);
|
|
243
|
+
} else {
|
|
244
|
+
await this.renderWithoutLayout(viewInstance, meta);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Render view wrapped in a layout
|
|
250
|
+
*/
|
|
251
|
+
private async renderWithLayout(viewInstance: AppView<any>, layoutHandler: string, meta?: any): Promise<void> {
|
|
252
|
+
const LayoutClass = this.layoutRegistry.get(layoutHandler);
|
|
253
|
+
|
|
254
|
+
if (!LayoutClass) {
|
|
255
|
+
console.error(`Layout not registered: ${layoutHandler}`);
|
|
256
|
+
// Fallback to no layout
|
|
257
|
+
await this.renderWithoutLayout(viewInstance, meta);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Create or reuse layout instance
|
|
262
|
+
if (!this.currentLayoutInstance || this.currentLayoutInstance.constructor !== LayoutClass) {
|
|
263
|
+
// Create new layout instance
|
|
264
|
+
const layoutInstance = new LayoutClass();
|
|
265
|
+
this.currentLayoutInstance = layoutInstance;
|
|
266
|
+
|
|
267
|
+
// Show layout in StackView
|
|
268
|
+
if (this.stackView) {
|
|
269
|
+
await this.stackView.begin(layoutInstance, meta?.effect);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Find content outlet in layout (should be a <stack-view> element)
|
|
274
|
+
const outlet = this.currentLayoutInstance.querySelector('[data-outlet="content"]') as StackView;
|
|
275
|
+
|
|
276
|
+
if (!outlet) {
|
|
277
|
+
console.error('Layout does not have a content outlet [data-outlet="content"]');
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Check if outlet is a stack-view, if not create one
|
|
282
|
+
let stackViewElement: StackView;
|
|
283
|
+
|
|
284
|
+
if (outlet.tagName.toLowerCase() === 'stack-view') {
|
|
285
|
+
stackViewElement = outlet;
|
|
286
|
+
} else {
|
|
287
|
+
// Outlet is not a stack-view, create one inside it
|
|
288
|
+
let existingStackView = outlet.querySelector('stack-view') as StackView;
|
|
289
|
+
|
|
290
|
+
if (!existingStackView) {
|
|
291
|
+
existingStackView = new StackView();
|
|
292
|
+
existingStackView.effect = 'fade';
|
|
293
|
+
existingStackView.backButton = false;
|
|
294
|
+
outlet.appendChild(existingStackView);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
stackViewElement = existingStackView;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Use stack-view's begin method for smooth transitions
|
|
301
|
+
await stackViewElement.begin(viewInstance, meta?.effect);
|
|
302
|
+
|
|
303
|
+
// Trigger view lifecycle
|
|
304
|
+
if (viewInstance.onMounted) {
|
|
305
|
+
await viewInstance.onMounted();
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Render view without layout
|
|
311
|
+
*/
|
|
312
|
+
private async renderWithoutLayout(viewInstance: AppView<any>, meta?: any): Promise<void> {
|
|
313
|
+
// Clear current layout if exists
|
|
314
|
+
this.currentLayoutInstance = null;
|
|
315
|
+
|
|
316
|
+
// Show view using StackView
|
|
317
|
+
if (this.stackView) {
|
|
318
|
+
await this.stackView.begin(viewInstance, meta?.effect);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Handle not found event from router
|
|
324
|
+
*/
|
|
325
|
+
private async handleNotFound(event: CustomEvent<NavigationEventDetail>): Promise<void> {
|
|
326
|
+
const { handler } = event.detail;
|
|
327
|
+
|
|
328
|
+
// Resolve view class from handler (can be class constructor or string)
|
|
329
|
+
const ViewClass = this.resolveViewClass(handler);
|
|
330
|
+
|
|
331
|
+
if (!ViewClass) {
|
|
332
|
+
console.error(`404 View not found or registered: ${handler}`);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Create view instance
|
|
337
|
+
const viewInstance = new ViewClass();
|
|
338
|
+
|
|
339
|
+
// Store current view instance
|
|
340
|
+
this.currentViewInstance = viewInstance;
|
|
341
|
+
|
|
342
|
+
// 404 pages typically don't use layouts, render directly
|
|
343
|
+
await this.renderWithoutLayout(viewInstance);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Get the route pattern that matches the given path
|
|
348
|
+
*/
|
|
349
|
+
public getRoutePattern(path: string): string | null {
|
|
350
|
+
// Iterate through all registered routes to find the matching pattern
|
|
351
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
352
|
+
for (const [pattern, _] of (this.router as any).routes) {
|
|
353
|
+
const route = (this.router as any).routes.get(pattern).route;
|
|
354
|
+
if (route.match(path) !== null) {
|
|
355
|
+
return pattern;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Get the current view instance
|
|
363
|
+
*/
|
|
364
|
+
get currentView(): AppView<any> | null {
|
|
365
|
+
return this.currentViewInstance;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Get the current route pattern (e.g., '/user/:id')
|
|
370
|
+
*/
|
|
371
|
+
get currentRoute(): string | null {
|
|
372
|
+
return this.currentRoutePath;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Navigate to a specific path
|
|
377
|
+
* @param path - The path to navigate to
|
|
378
|
+
*/
|
|
379
|
+
navigate(path: string): void {
|
|
380
|
+
this.router.navigate(path);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Go back in navigation history
|
|
385
|
+
*/
|
|
386
|
+
async goBack(): Promise<void> {
|
|
387
|
+
if (this.stackView && this.stackView.canGoBack()) {
|
|
388
|
+
await this.stackView.complete();
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|