application.ts 1.0.1 → 1.0.3

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.
@@ -1,284 +0,0 @@
1
- import { Route } from './route';
2
- import type { RouteParams, RouteOptions, RouteHandler, NavigationEventDetail } from './types';
3
- import { NavigationEvents } from './types';
4
-
5
- /**
6
- * Router class for managing navigation and route mapping
7
- * Emits events for Application.ts to handle view rendering via StackView
8
- */
9
- export class Router extends EventTarget {
10
- private routes: Map<string, { route: Route; handler: RouteHandler }> = new Map();
11
- private notFoundHandler?: RouteHandler;
12
- private isInitialized: boolean = false;
13
- private basePath: string = '';
14
- private _currentPath: string = '/';
15
- private _currentParams: RouteParams = {};
16
- private _currentMeta?: Record<string, any>;
17
-
18
- constructor() {
19
- super();
20
-
21
- // Listen for popstate events (browser back/forward)
22
- window.addEventListener('popstate', () => {
23
- this.handleRouteChange();
24
- });
25
-
26
- // Intercept link clicks for client-side navigation
27
- document.addEventListener('click', (e) => {
28
- const target = e.target as HTMLElement;
29
- const link = target.closest('a[href]') as HTMLAnchorElement;
30
-
31
- if (link && link.href.startsWith(window.location.origin) && !link.hasAttribute('target')) {
32
- e.preventDefault();
33
- this.navigate(link.pathname);
34
- }
35
- });
36
- }
37
-
38
- /**
39
- * Map a route path to a handler (view identifier)
40
- * @param path - The route path (e.g., '/', '/user/:id')
41
- * @param handler - The view identifier/handler
42
- * @param options - Optional route configuration
43
- * @returns The router instance for chaining
44
- */
45
- map(path: string, handler: RouteHandler, options?: RouteOptions): this {
46
- const route = new Route(path, options);
47
- this.routes.set(path, { route, handler });
48
- return this;
49
- }
50
-
51
- /**
52
- * Set the 404 not found handler
53
- * @param handler - The view identifier to use for 404
54
- * @returns The router instance for chaining
55
- */
56
- notFound(handler: RouteHandler): this {
57
- this.notFoundHandler = handler;
58
- return this;
59
- }
60
-
61
- /**
62
- * Set the base path for all routes
63
- * @param base - Base path (e.g., '/basic', '/app')
64
- * @returns The router instance for chaining
65
- */
66
- setBasePath(base: string): this {
67
- // Normalize: ensure starts with / and no trailing /
68
- this.basePath = base.replace(/\/$/, '').replace(/^(?!\/)/, '/');
69
- return this;
70
- }
71
-
72
- /**
73
- * Get the current base path
74
- */
75
- getBasePath(): string {
76
- return this.basePath;
77
- }
78
-
79
- /**
80
- * Initialize the router and handle the current path
81
- */
82
- start(): void {
83
- if (this.isInitialized) {
84
- console.warn('Router is already initialized');
85
- return;
86
- }
87
-
88
- // Auto-detect base path from <base> tag if not set
89
- if (!this.basePath) {
90
- const baseTag = document.querySelector('base[href]') as HTMLBaseElement;
91
- if (baseTag) {
92
- const baseHref = baseTag.getAttribute('href') || '';
93
- // Extract path from href (could be full URL or relative path)
94
- try {
95
- const url = new URL(baseHref, window.location.origin);
96
- this.basePath = url.pathname.replace(/\/$/, '');
97
- } catch {
98
- this.basePath = baseHref.replace(/\/$/, '');
99
- }
100
- }
101
- }
102
-
103
- this.isInitialized = true;
104
- this.handleRouteChange();
105
- }
106
-
107
- /**
108
- * Navigate to a specific path
109
- * @param path - The path to navigate to (relative to basePath)
110
- * @param replaceState - If true, replaces current history entry instead of pushing
111
- */
112
- async navigate(path: string, replaceState: boolean = false): Promise<void> {
113
- // Strip basePath if present to get route path
114
- const routePath = this.stripBasePath(path);
115
-
116
- // Find matching route
117
- const match = this.findRoute(routePath);
118
-
119
- if (!match) {
120
- this.emitNotFound(routePath);
121
- return;
122
- }
123
-
124
- const { route, handler, params } = match;
125
-
126
- // Check route guard
127
- const guardResult = await route.canEnter(params);
128
-
129
- if (guardResult === false) {
130
- // Navigation denied
131
- console.warn(`Navigation to ${path} was denied by route guard`);
132
- return;
133
- }
134
-
135
- if (typeof guardResult === 'string') {
136
- // Redirect to another path
137
- await this.navigate(guardResult, replaceState);
138
- return;
139
- }
140
-
141
- // Add basePath to create full URL
142
- const fullPath = this.addBasePath(routePath);
143
-
144
- // Update browser history
145
- if (replaceState) {
146
- window.history.replaceState({ path: fullPath }, '', fullPath);
147
- } else {
148
- window.history.pushState({ path: fullPath }, '', fullPath);
149
- }
150
-
151
- // Emit navigation event for Application.ts to handle
152
- this.emitNavigation(routePath, params, handler, route.options.meta);
153
- }
154
-
155
- /**
156
- * Get the current path (route pattern)
157
- */
158
- get currentPath(): string {
159
- return this._currentPath;
160
- }
161
-
162
- /**
163
- * Get the current route parameters
164
- */
165
- get currentParams(): RouteParams {
166
- return this._currentParams;
167
- }
168
-
169
- /**
170
- * Get the current route metadata
171
- */
172
- get currentMeta(): Record<string, any> | undefined {
173
- return this._currentMeta;
174
- }
175
-
176
- /**
177
- * Find a route that matches the given path
178
- */
179
- private findRoute(path: string): { route: Route; handler: RouteHandler; params: RouteParams } | null {
180
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
181
- for (const [_, { route, handler }] of this.routes) {
182
- const params = route.match(path);
183
- if (params !== null) {
184
- return { route, handler, params };
185
- }
186
- }
187
- return null;
188
- }
189
-
190
- /**
191
- * Handle route change (from popstate or initial load)
192
- */
193
- private async handleRouteChange(): Promise<void> {
194
- const routePath = this.stripBasePath(window.location.pathname);
195
- await this.navigate(routePath, true);
196
- }
197
-
198
- /**
199
- * Strip base path from a full path
200
- */
201
- private stripBasePath(fullPath: string): string {
202
- if (!this.basePath) {
203
- return fullPath;
204
- }
205
-
206
- if (fullPath.startsWith(this.basePath)) {
207
- const stripped = fullPath.slice(this.basePath.length);
208
- return stripped || '/';
209
- }
210
-
211
- return fullPath;
212
- }
213
-
214
- /**
215
- * Add base path to a route path
216
- */
217
- private addBasePath(routePath: string): string {
218
- if (!this.basePath) {
219
- return routePath;
220
- }
221
-
222
- // Ensure route path starts with /
223
- const normalizedPath = routePath.startsWith('/') ? routePath : '/' + routePath;
224
- return this.basePath + normalizedPath;
225
- }
226
-
227
- /**
228
- * Emit navigation event
229
- */
230
- private emitNavigation(path: string, params: RouteParams, handler: RouteHandler, meta?: Record<string, any>): void {
231
- // Store current navigation state
232
- this._currentPath = path;
233
- this._currentParams = params;
234
- this._currentMeta = meta;
235
-
236
- const detail: NavigationEventDetail = {
237
- path,
238
- params,
239
- handler,
240
- meta
241
- };
242
-
243
- this.dispatchEvent(new CustomEvent(NavigationEvents.NAVIGATE, { detail }));
244
- window.dispatchEvent(new CustomEvent(NavigationEvents.NAVIGATE, { detail }));
245
- }
246
-
247
- /**
248
- * Emit not found event
249
- */
250
- private emitNotFound(path: string): void {
251
- if (this.notFoundHandler) {
252
- // Store current navigation state
253
- this._currentPath = path;
254
- this._currentParams = {};
255
- this._currentMeta = undefined;
256
-
257
- const detail: NavigationEventDetail = {
258
- path,
259
- params: {},
260
- handler: this.notFoundHandler
261
- };
262
- this.dispatchEvent(new CustomEvent(NavigationEvents.NOT_FOUND, { detail }));
263
- window.dispatchEvent(new CustomEvent(NavigationEvents.NOT_FOUND, { detail }));
264
- } else {
265
- console.error(`404: No route found for ${path} and no notFound handler configured`);
266
- }
267
- }
268
-
269
- /**
270
- * Generate a URL for a route with parameters
271
- * @param path - The route path pattern
272
- * @param params - Parameters to inject
273
- * @returns The generated URL (with basePath) or null if route not found
274
- */
275
- generateUrl(path: string, params: RouteParams = {}): string | null {
276
- const routeEntry = this.routes.get(path);
277
- if (!routeEntry) {
278
- return null;
279
- }
280
-
281
- const routePath = routeEntry.route.generate(params);
282
- return this.addBasePath(routePath);
283
- }
284
- }
@@ -1,73 +0,0 @@
1
- /**
2
- * Route parameters extracted from URL path
3
- */
4
- export type RouteParams = Record<string, string>;
5
-
6
- /**
7
- * Navigation guard function that can prevent navigation
8
- * @returns true to allow navigation, false to prevent, or string path to redirect
9
- */
10
- export type RouteGuard = (params: RouteParams) => boolean | string | Promise<boolean | string>;
11
-
12
- /**
13
- * Route configuration options
14
- */
15
- export interface RouteOptions {
16
- /**
17
- * Guard function to check if navigation is allowed
18
- */
19
- canEnter?: RouteGuard;
20
-
21
- /**
22
- * Additional metadata for the route
23
- */
24
- meta?: Record<string, any>;
25
- }
26
-
27
- /**
28
- * Route handler - reference to view class constructor or string identifier
29
- */
30
- export type RouteHandler<T = any> = (new () => T) | string;
31
-
32
- /**
33
- * Route definition
34
- */
35
- export interface RouteDefinition {
36
- path: string;
37
- handler: RouteHandler;
38
- options?: RouteOptions;
39
- }
40
-
41
- /**
42
- * Navigation event detail
43
- */
44
- export interface NavigationEventDetail {
45
- /**
46
- * The path being navigated to
47
- */
48
- path: string;
49
-
50
- /**
51
- * Route parameters extracted from the path
52
- */
53
- params: RouteParams;
54
-
55
- /**
56
- * The route handler (view identifier)
57
- */
58
- handler: RouteHandler;
59
-
60
- /**
61
- * Route metadata
62
- */
63
- meta?: Record<string, any>;
64
- }
65
-
66
- /**
67
- * Navigation events
68
- */
69
- export const NavigationEvents = {
70
- BEFORE_NAVIGATE: 'navigation:before',
71
- NAVIGATE: 'navigation:navigate',
72
- NOT_FOUND: 'navigation:notfound'
73
- } as const;