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,359 +0,0 @@
1
- import { TemplateBinder } from 'template.ts';
2
- import type { RouteParams } from '../navigation/types';
3
- import { App } from './App';
4
- import type {
5
- AppViewLifecycle,
6
- AppViewOptions,
7
- AppViewState
8
- } from './types';
9
-
10
- /**
11
- * Abstract base class for creating views with Template.Ts
12
- * Extends HTMLElement as a Web Component for seamless integration with StackView.Ts
13
- * Custom elements are automatically registered when views are registered with App
14
- *
15
- * @example
16
- * ```typescript
17
- * const template: `
18
- * <div>
19
- * <h1>Count: {{ count }}</h1>
20
- * <button @on:click="increment">Increment</button>
21
- * </div>`;
22
- *
23
- * class State {
24
- * count: number = 0;
25
- * increment: () => { this.count += 1; };
26
- * }
27
- *
28
- * @Register
29
- * class HomeView extends AppView<{ count: number }> {
30
- * template(): string {
31
- * return template;
32
- * }
33
- *
34
- * state() {
35
- * return new State();
36
- * }
37
- * }
38
- *
39
- * // Register with app - automatically registers as <home-view>
40
- * app.registerView('HomeView', HomeView);
41
- * ```
42
- */
43
- export abstract class AppView<TState extends AppViewState = AppViewState> extends HTMLElement implements AppViewLifecycle {
44
- protected binder: TemplateBinder | null = null;
45
- protected _state: TState | null = null;
46
- protected _options: AppViewOptions;
47
- protected _root: HTMLElement | ShadowRoot;
48
- private _isInitialized: boolean = false;
49
-
50
- constructor(options?: AppViewOptions) {
51
- super();
52
-
53
- this._options = {
54
- transitionClass: 'transition-fade',
55
- autoUpdate: true,
56
- useShadowDOM: false,
57
- ...options
58
- };
59
-
60
- // Create shadow root if enabled
61
- if (this._options.useShadowDOM) {
62
- const shadow = this.attachShadow({ mode: 'open' });
63
- shadow.innerHTML = this.template();
64
- if (!shadow.firstElementChild || !(shadow.firstElementChild instanceof HTMLElement)) {
65
- throw new Error('AppView template must have a single root element');
66
- }
67
- this._root = shadow.firstElementChild as HTMLElement;
68
- } else {
69
- this.innerHTML = this.template();
70
- if (!this.firstElementChild || !(this.firstElementChild instanceof HTMLElement)) {
71
- throw new Error('AppView template must have a single root element');
72
- }
73
- this._root = this.firstElementChild as HTMLElement;
74
- }
75
-
76
- // Eager initialization in constructor
77
- this.initialize();
78
- }
79
-
80
- /**
81
- * Render the view with route parameters
82
- * Initializes the template and binds state
83
- * @param params - Route parameters from the router
84
- */
85
- initialize(): void {
86
- if (this._isInitialized) {
87
- return;
88
- }
89
-
90
- // Add app-view class
91
- this.classList.add('app-view');
92
-
93
- // Initialize state
94
- this._state = this.state();
95
-
96
- // Bind template using container
97
- this.binder = new TemplateBinder(
98
- this._root,
99
- this._state,
100
- this._options.transitionClass
101
- );
102
- this.binder.autoUpdate = this._options.autoUpdate ?? true;
103
-
104
- this.binder.bind();
105
-
106
- this._isInitialized = true;
107
- }
108
-
109
- /**
110
- * Get the custom element tag name for this class
111
- * Uses explicit tagName property or falls back to class name conversion
112
- * To prevent minification issues, define static tagName property in your class
113
- */
114
- static getTagName(): string {
115
- // Use explicit tagName if provided (prevents minification issues)
116
- if ((this as any).tagName && typeof (this as any).tagName === 'string') {
117
- return (this as any).tagName;
118
- }
119
-
120
- // Fallback to class name conversion (may break with minification)
121
- return this.name
122
- .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
123
- .toLowerCase();
124
- }
125
-
126
- /**
127
- * Register this class as a custom element
128
- * Should be called before instantiation
129
- */
130
- static register(): void {
131
- const tagName = this.getTagName();
132
- if (!customElements.get(tagName)) {
133
- customElements.define(tagName, this as any);
134
- }
135
- }
136
-
137
- /**
138
- * Define the HTML template for this view
139
- * Use Template.Ts syntax for data binding
140
- * @returns HTML template string
141
- */
142
- protected abstract template(): string;
143
-
144
- /**
145
- * Define the initial state for this view
146
- * State should include data and methods
147
- * @returns Initial state object
148
- */
149
- protected abstract state(): TState;
150
-
151
- /**
152
- * Get the current state
153
- */
154
- protected get viewState(): TState {
155
- if (!this._state) {
156
- throw new Error('State not initialized. Call render() first.');
157
- }
158
- return this._state;
159
- }
160
-
161
- /**
162
- * Get the route parameters passed to this view
163
- */
164
- protected get params(): RouteParams {
165
- return this.app?.router?.currentParams || {};
166
- }
167
-
168
- /**
169
- * Get the App instance by traversing up the DOM tree
170
- */
171
- protected get app(): App | null {
172
- return App.fromElement(this);
173
- }
174
-
175
- /**
176
- * Navigate to a specific path using the app's router
177
- * @param path - The path to navigate to
178
- */
179
- protected navigate(path: string): void {
180
- const appInstance = this.app;
181
- if (appInstance) {
182
- appInstance.navigate(path);
183
- } else {
184
- console.warn('Cannot navigate: App instance not found');
185
- }
186
- }
187
-
188
- /**
189
- * Update a single state value
190
- * @param key - The state key to update
191
- * @param value - The new value
192
- */
193
- protected setState<K extends keyof TState>(key: K, value: TState[K]): void {
194
- if (!this._state) {
195
- console.warn('Cannot set state before initialization');
196
- return;
197
- }
198
-
199
- this._state[key] = value;
200
-
201
- // Call lifecycle hook
202
- if (this.onStateChanged) {
203
- this.onStateChanged(key as string, value);
204
- }
205
-
206
- // Auto-update if enabled
207
- if (this._options.autoUpdate && this.binder) {
208
- this.update();
209
- }
210
- }
211
-
212
- /**
213
- * Update multiple state values at once
214
- * @param updates - Object with state updates
215
- */
216
- protected setStates(updates: Partial<TState>): void {
217
- if (!this._state) {
218
- console.warn('Cannot set state before initialization');
219
- return;
220
- }
221
-
222
- Object.assign(this._state, updates);
223
-
224
- // Call lifecycle hooks for each update
225
- if (this.onStateChanged) {
226
- for (const [key, value] of Object.entries(updates)) {
227
- this.onStateChanged(key, value);
228
- }
229
- }
230
-
231
- // Auto-update if enabled
232
- if (this._options.autoUpdate && this.binder) {
233
- this.update();
234
- }
235
- }
236
-
237
- /**
238
- * Manually trigger a view update
239
- * @param withAnimation - Whether to apply transition animation
240
- */
241
- public update(withAnimation: boolean = true): void {
242
- if (this.binder) {
243
- this.binder.update(withAnimation);
244
- }
245
- }
246
-
247
- /**
248
- * Update route parameters and re-trigger initialization logic
249
- * Used when navigating to the same view with different parameters
250
- * @param params - New route parameters
251
- */
252
- async updateParams(params: RouteParams = {}): Promise<void> {
253
- // Call the parameter changed hook with old and new params
254
- if (this.onParamsChanged) {
255
- await this.onParamsChanged(params, this.params);
256
- }
257
-
258
- // Update the view if needed
259
- if (this.binder && this._options.autoUpdate) {
260
- this.update();
261
- }
262
- }
263
-
264
- /**
265
- * StackView lifecycle: called when view is about to be shown
266
- */
267
- async stackViewShowing(): Promise<void> {
268
- // Call before mount hook
269
- if (this.onBeforeMount) {
270
- await this.onBeforeMount();
271
- }
272
- }
273
-
274
- /**
275
- * StackView lifecycle: called when view is about to be hidden
276
- */
277
- async stackViewHiding(): Promise<void> {
278
- if (this.onBeforeUnmount) {
279
- await this.onBeforeUnmount();
280
- }
281
- }
282
-
283
- /**
284
- * StackView lifecycle: called after view is hidden
285
- */
286
- async stackViewHidden(): Promise<void> {
287
- // Destroy binder
288
- if (this.binder) {
289
- this.binder.destroy();
290
- this.binder = null;
291
- }
292
-
293
- this._isInitialized = false;
294
-
295
- if (this.onUnmounted) {
296
- await this.onUnmounted();
297
- }
298
- }
299
-
300
- /**
301
- * Web Component lifecycle: called when connected to DOM
302
- */
303
- async connectedCallback(): Promise<void> {
304
- // Initialize if not already done
305
- if (!this._isInitialized) {
306
- await this.initialize();
307
- }
308
-
309
- if (this.onMounted) {
310
- await this.onMounted();
311
- }
312
- }
313
-
314
- /**
315
- * Web Component lifecycle: called when disconnected from DOM
316
- */
317
- disconnectedCallback(): void {
318
- // Cleanup is handled by stackViewHidden
319
- }
320
-
321
- /**
322
- * Check if the view is initialized
323
- */
324
- get isInitialized(): boolean {
325
- return this._isInitialized;
326
- }
327
-
328
- // Lifecycle hooks (can be overridden by subclasses)
329
- onBeforeMount?(): void | Promise<void>;
330
- onMounted?(): void | Promise<void>;
331
- onBeforeUnmount?(): void | Promise<void>;
332
- onUnmounted?(): void | Promise<void>;
333
- onStateChanged?(key: string, value: any): void;
334
- onParamsChanged?(newParams: RouteParams, oldParams: RouteParams): void | Promise<void>;
335
- }
336
-
337
- /**
338
- * Decorator to register a view as a custom element
339
- * Optionally accepts a custom tag name to prevent minification issues
340
- *
341
- * @example
342
- * @Register() // Auto-generates tag name from class name
343
- * @Register('my-component') // Explicit tag name (recommended for production)
344
- */
345
- export function Register(tagName?: string) : any {
346
- return function(target: any) {
347
- // Use provided tag name or generate from class name
348
- if (tagName) {
349
- target.tagName = tagName;
350
- } else if (!target.tagName) {
351
- // Fallback to class name conversion (may fail with minification)
352
- target.tagName = target.name
353
- .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
354
- .toLowerCase();
355
- }
356
- target.register();
357
- return target;
358
- };
359
- }
package/src/app/types.ts DELETED
@@ -1,78 +0,0 @@
1
- import type { RouteParams } from '../navigation/types';
2
-
3
- /**
4
- * AppView lifecycle hooks
5
- */
6
- export interface AppViewLifecycle {
7
- /**
8
- * Called before the view is shown
9
- * Use this to initialize data, fetch from APIs, etc.
10
- */
11
- onBeforeMount?(): void | Promise<void>;
12
-
13
- /**
14
- * Called after the view is mounted to the DOM
15
- * Use this to set up event listeners, third-party libraries, etc.
16
- */
17
- onMounted?(): void | Promise<void>;
18
-
19
- /**
20
- * Called before the view is unmounted
21
- * Use this to clean up resources, save state, etc.
22
- */
23
- onBeforeUnmount?(): void | Promise<void>;
24
-
25
- /**
26
- * Called after the view is unmounted from the DOM
27
- * Final cleanup
28
- */
29
- onUnmounted?(): void | Promise<void>;
30
-
31
- /**
32
- * Called when the state is updated
33
- * Use this to react to state changes
34
- */
35
- onStateChanged?(key: string, value: any): void;
36
-
37
- /**
38
- * Called when route parameters change while staying on the same view
39
- * Use this to reload data based on new parameters (e.g., /user/1 -> /user/2)
40
- * @param newParams - The new route parameters
41
- * @param oldParams - The previous route parameters
42
- */
43
- onParamsChanged?(newParams: RouteParams, oldParams: RouteParams): void | Promise<void>;
44
- }
45
-
46
- /**
47
- * AppView configuration options
48
- */
49
- export interface AppViewOptions {
50
- /**
51
- * Custom transition class for Template.Ts animations
52
- * Default: 'transition-fade'
53
- */
54
- transitionClass?: string;
55
-
56
- /**
57
- * Whether to automatically update the view on state changes
58
- * Default: true
59
- */
60
- autoUpdate?: boolean;
61
-
62
- /**
63
- * Whether to use Shadow DOM for the template
64
- * When true, template is rendered in shadow root (enables <slot> to work properly)
65
- * Default: false
66
- */
67
- useShadowDOM?: boolean;
68
- }
69
-
70
- /**
71
- * AppView state object - can be any shape defined by the developer
72
- */
73
- export type AppViewState = Record<string, any>;
74
-
75
- /**
76
- * Template function that returns HTML string
77
- */
78
- export type TemplateFunction<T extends AppViewState = AppViewState> = (this: T) => string;
package/src/index.ts DELETED
@@ -1,28 +0,0 @@
1
- /**
2
- * Main entry point for the application
3
- * Export all public APIs
4
- */
5
-
6
- export { App } from './app/App';
7
- export { AppView, Register } from './app/AppView';
8
- export { Router } from './navigation/router';
9
- export { Route } from './navigation/route';
10
-
11
- // Export types
12
- export type {
13
- AppViewLifecycle,
14
- AppViewOptions,
15
- AppViewState,
16
- TemplateFunction
17
- } from './app/types';
18
-
19
- export type {
20
- RouteParams,
21
- RouteGuard,
22
- RouteOptions,
23
- RouteHandler,
24
- RouteDefinition,
25
- NavigationEventDetail
26
- } from './navigation/types';
27
-
28
- export { NavigationEvents } from './navigation/types';
@@ -1,87 +0,0 @@
1
- import type { RouteParams, RouteOptions } from './types';
2
-
3
- /**
4
- * Represents a route with path pattern matching and parameter extraction
5
- */
6
- export class Route {
7
- public readonly path: string;
8
- public readonly pattern: RegExp;
9
- public readonly paramNames: string[];
10
- public readonly options: RouteOptions;
11
-
12
- constructor(path: string, options: RouteOptions = {}) {
13
- this.path = path;
14
- this.options = options;
15
- this.paramNames = [];
16
-
17
- // Convert path pattern to RegExp and extract parameter names
18
- this.pattern = this.pathToRegExp(path);
19
- }
20
-
21
- /**
22
- * Convert a path pattern like '/user/:id' to a RegExp
23
- * Extracts parameter names like 'id'
24
- */
25
- private pathToRegExp(path: string): RegExp {
26
- // Escape special characters except for :param patterns
27
- const escapedPath = path.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
28
-
29
- // Replace :param with capture groups and extract param names
30
- const pattern = escapedPath.replace(/:([^/]+)/g, (match, paramName) => {
31
- this.paramNames.push(paramName);
32
- return '([^/]+)';
33
- });
34
-
35
- // Match exact path or with trailing slash
36
- return new RegExp(`^${pattern}/?$`);
37
- }
38
-
39
- /**
40
- * Check if the given path matches this route
41
- * @param path - The path to test
42
- * @returns The extracted parameters if match, null otherwise
43
- */
44
- match(path: string): RouteParams | null {
45
- const match = this.pattern.exec(path);
46
-
47
- if (!match) {
48
- return null;
49
- }
50
-
51
- // Extract parameters from capture groups
52
- const params: RouteParams = {};
53
- this.paramNames.forEach((name, index) => {
54
- params[name] = decodeURIComponent(match[index + 1]);
55
- });
56
-
57
- return params;
58
- }
59
-
60
- /**
61
- * Check if navigation is allowed via the route guard
62
- * @param params - Route parameters
63
- * @returns true if allowed, false if denied, or redirect path
64
- */
65
- async canEnter(params: RouteParams): Promise<boolean | string> {
66
- if (!this.options.canEnter) {
67
- return true;
68
- }
69
-
70
- return await this.options.canEnter(params);
71
- }
72
-
73
- /**
74
- * Generate a path from this route pattern with given parameters
75
- * @param params - Parameters to inject into the path
76
- * @returns The generated path
77
- */
78
- generate(params: RouteParams = {}): string {
79
- let path = this.path;
80
-
81
- for (const [key, value] of Object.entries(params)) {
82
- path = path.replace(`:${key}`, encodeURIComponent(value));
83
- }
84
-
85
- return path;
86
- }
87
- }