@tmjeee/w-lib 0.0.1 → 0.1.1

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/CHANGELOG.md CHANGED
@@ -5,18 +5,21 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [0.0.1] - 2026-05-27
9
8
 
10
- ### Added
9
+ ## [0.1.1] - 2026-06-01
10
+ - added `template-selection` , a convenient Angular template marker
11
+ - separate each utilities into it's own example file
12
+
13
+ ## [0.1.0] - 2026-05-29
14
+ - added initial version of
15
+ - event emitter
16
+ - state management
17
+ - finite state machine
18
+ - loading states
19
+
20
+ ## [0.0.1] - 2026-05-27
11
21
  - Initial library skeleton for npm publishing
12
- - `helloWorld()` sample function exported as the public API
13
- - Full ESM + CJS dual package support via tsdown
22
+ - Full ESM package support via tsdown
14
23
  - TypeScript strict configuration
15
24
  - Vitest test suite with coverage
16
- - `examples/basic.ts` runnable usage example
17
- - Complete publish-ready `package.json` (exports map, files, sideEffects, publishConfig)
18
25
  - MIT license and minimal conventional changelog
19
-
20
- ### Notes
21
- - This is the bootstrap release of the w-lib skeleton.
22
- - Ready for `npm publish --access public`.
package/README.md CHANGED
@@ -3,9 +3,11 @@
3
3
  [![npm version](https://img.shields.io/npm/v/@tmjeee/w-lib.svg)](https://www.npmjs.com/package/@tmjeee/w-lib)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
5
 
6
- A minimal, modern TypeScript library skeleton ready for publishing to npmjs.com.
7
6
 
8
- This package demonstrates current (2026) best practices for shipping small, tree-shakeable, dual-format (ESM + CJS) libraries with excellent TypeScript support.
7
+ ## Development
8
+ See [here](./README_DEV.md) for more info.
9
+
10
+ A minimal, Angular and TypeScript library.
9
11
 
10
12
  ## Installation
11
13
 
@@ -17,164 +19,221 @@ npm install @tmjeee/w-lib
17
19
 
18
20
  This package is **ESM-only**.
19
21
 
20
- ```ts
21
- import { helloWorld } from '@tmjeee/w-lib';
22
-
23
- console.log(helloWorld()); // "Hello, world!"
24
- ```
25
-
26
- > **Note**: CommonJS (`require()`) is not supported.
27
- > If you are in a CommonJS environment, use dynamic import instead:
28
- > ```js
29
- > const { helloWorld } = await import('@tmjeee/w-lib');
30
- > ```
31
-
32
- For more information, see the [Node.js documentation on publishing ESM packages](https://nodejs.org/en/learn/modules/publishing-a-package).
33
22
 
34
- ## API
23
+ ### loading-state (Angular)
24
+ ```typescript
25
+ import {createLoadingState} from '@tmjeee/w-lib';
35
26
 
36
- ### `helloWorld(): string`
27
+ const loadingState = createLoadingState();
28
+ loadingState.withLoading('assets', async ()=>{
29
+ // xhr calls, long running calls
30
+ // loadingState.is('asset'); will be true while this function is running else false
31
+ });
37
32
 
38
- Returns the classic greeting.
33
+ const isLoading = loadingState.is('assets'); // signal<boolean> - true when loading else false
39
34
 
40
- ```ts
41
- helloWorld(); // "Hello, world!"
35
+ loadingState.set('assets', false); // explicitly mark 'asset' as false
42
36
  ```
43
37
 
44
- ## Development
45
-
46
- ```bash
47
- # Install dependencies
48
- npm install
49
-
50
- # Type check
51
- npm run typecheck
52
-
53
- # Run tests (once, exits after completion)
54
- npm test
55
-
56
- # Run tests in watch mode (for development)
57
- npm run test:watch
58
-
59
- # Run tests with coverage
60
- npm run test:coverage
61
-
62
- # Build (outputs to dist/)
63
- npm run build
64
-
65
- # Watch mode during development
66
- npm run dev
38
+ ### Finite state machine (Generic)
39
+ ```typescript
40
+ import {FsmState, Fsm} from '@tmjeee/w-lib';
41
+
42
+ class State1 extends FsmState {
43
+ enter(prevState: FsmState | null, ...params: any) {
44
+ console.log(`[State1] enter`);
45
+ }
46
+ exit(nextState: FsmState | null, ...params: any) {
47
+ console.log(`[State1] exit`);
48
+ }
49
+ update(...params: any) {
50
+ console.log(`[State1] update`);
51
+ }
52
+ }
53
+
54
+
55
+ class State2 extends FsmState {
56
+ enter(prevState: FsmState | null, ...params: any) {
57
+ console.log(`[State2] enter`);
58
+ }
59
+ exit(nextState: FsmState | null, ...params: any) {
60
+ console.log(`[State2] exit`);
61
+ }
62
+ update(...params: any) {
63
+ console.log(`[State2] update`);
64
+ }
65
+ }
66
+
67
+ const fsm = new Fsm();
68
+ fsm.register(
69
+ new State1(`state1`),
70
+ new State2(`state2`),
71
+ );
72
+
73
+ fsm.set('state1', {}); // state1.enter(...)
74
+ fsm.update(); // state1.update({})
75
+ fsm.set('state2'); // state1.exit(...) then state2.enter(...)
76
+ fsm.update(); // state2.update({});
77
+ fsm.get(); // return current state -> state2
67
78
  ```
68
79
 
69
- ### Project Structure
70
80
 
81
+ ### State Management (Generic)
82
+ ```typescript
83
+ const store = createStateStore<State>({
84
+ name: 'jim',
85
+ age: 12,
86
+ address: {
87
+ address1: '1 Kent Street',
88
+ address2: 'Sydney',
89
+ postcode: '2000',
90
+ }
91
+ });
92
+
93
+
94
+ const address1 = store.get('address.address1');
95
+ store.on('change', (change) => {
96
+ const state = change.state;
97
+ const previousState = change.previousState;
98
+ const value = change.value;
99
+ const previousValue = change.previousValue;
100
+
101
+ console.log(`change`, state, previousState, value, previousValue);
102
+ });
103
+
104
+ // all the following will trigger 'change' event
105
+ store.update('address', (address) => ({address1: 'new address1', address2: 'new address2', postcode: 'new postcode'}));
106
+ store.update('name', (name) => `name-${new Date()}`);
107
+ store.set('name', 'name1');
108
+ store.patch({age: 2, name: 'test'});
109
+ store.batch((ctx) => {
110
+ ctx.set('age', 1);
111
+ ctx.update('age', (age)=>age + 1);
112
+ ctx.patch({age: 2, name: 'test'});
113
+ });
71
114
  ```
72
- src/index.ts # Source (helloWorld)
73
- test/index.test.ts # Vitest tests
74
- examples/basic.ts # Runnable usage demo
75
- dist/ # Build output (generated)
76
- ```
77
-
78
- ## Publishing
79
115
 
80
- This package is configured for immediate publishing.
81
116
 
82
- ### Recommended: One-command publish
83
-
84
- ```bash
85
- # Bump version first (patch / minor / major)
86
- npm version patch
87
-
88
- # Dry-run first (highly recommended)
89
- npm run publish:npm:dry
90
-
91
- # Then publish for real
92
- npm run publish:npm
117
+ ### Event Emitter (Generic)
118
+ ```typescript
119
+ const emitter1 = new JsEventEmitter();
120
+ const emitter2 = new JsEventEmitter();
121
+ emitter1.on('test',()=>{
122
+ console.log(`[Emitter1] Receive test event`);
123
+ });
124
+ emitter2.once('test', ()=>{
125
+ console.log(`[Emitter2] Receive test event - once only`);
126
+ });
127
+ emitter1.emit('test', {test: 'test1'});
128
+ emitter1.emit('test', {test: 'test2'});
129
+ emitter2.emit('test', {test: 'test1'});
130
+ emitter2.emit('test', {test: 'test2'});
131
+ emitter1.off('test'); // turn off listening
132
+ emitter1.emit('test', {test: 'test3'});
93
133
  ```
94
134
 
95
- ### Manual / step-by-step (with safety checks)
96
-
97
- ```bash
98
- # 1. Make sure everything is clean
99
- npm run typecheck && npm test && npm run build
100
- # (npm test now runs once and exits automatically)
101
135
 
102
- # 2. Dry-run the publish (strongly recommended)
103
- npm publish --dry-run --access public
104
136
 
105
- # 3. Bump version
106
- npm version patch
137
+ ### TemplateSelection Directive (Angular)
107
138
 
108
- # 4. Publish
109
- npm publish --access public
110
- ```
139
+ A lightweight directive that marks a template (or element) with a name. This allows you to define multiple named templates and then query + render them manually at runtime.
111
140
 
112
- The `prepublishOnly` hook (and both `publish:npm*` scripts) ensure that `build` + `test` run before anything is published.
141
+ It is especially useful when you want to let a component support multiple "slots" or conditional template rendering without hardcoding them.
113
142
 
114
- ### What gets published?
143
+ #### Import
115
144
 
116
- Only the contents of the `dist/` folder plus `package.json`, `README.md`, `LICENSE`, and `CHANGELOG.md` (controlled via the `"files"` field).
145
+ ```ts
146
+ import { TemplateSelection } from '@tmjeee/w-lib';
147
+ ```
117
148
 
118
- ### Automated publishing with GitHub Actions
149
+ #### Supported Syntax
119
150
 
120
- This repository includes two GitHub Actions workflows:
151
+ You can use it in two ways:
121
152
 
122
- - **`.github/workflows/ci.yml`** — Runs type checking and tests on every push and pull request. Can also be triggered manually from the Actions tab.
123
- - **`.github/workflows/publish.yml`** Publishes to npmjs.com when you create a new GitHub Release. Can also be triggered manually from the Actions tab (use with caution).
153
+ ```html
154
+ <!-- Recommended: on ng-template -->
155
+ <ng-template templateSelection="header">Header content</ng-template>
156
+ <ng-template templateSelection="footer">Footer content</ng-template>
124
157
 
125
- #### Setup steps (one time)
158
+ <!-- Also supported: structural directive syntax -->
159
+ <div *templateSelection="'sidebar'">Sidebar content</div>
160
+ ```
126
161
 
127
- 1. Create an npm access token:
128
- - Go to https://www.npmjs.com/settings/~tokens
129
- - Generate a new **Automation** token (or **Granular** with publish permission for `@tmjeee/w-lib`)
130
- 2. Add the token as a repository secret:
131
- - In your GitHub repo → **Settings → Secrets and variables → Actions**
132
- - Create a new secret named `NPM_TOKEN` with the value from step 1
133
- 3. (Optional but recommended) Enable npm **Trusted Publishing** (OIDC) for passwordless publishing:
134
- - See the comments in `publish.yml` and https://docs.npmjs.com/trusted-publishers
162
+ You can also bind the name dynamically:
135
163
 
136
- #### How to release
164
+ ```html
165
+ <ng-template [templateSelection]="templateName">...</ng-template>
166
+ ```
137
167
 
138
- ```bash
139
- # 1. Update CHANGELOG.md
140
- # 2. Commit everything
141
- git add .
142
- git commit -m "chore: prepare v0.0.2"
168
+ #### Querying Templates
143
169
 
144
- # 3. Create a version tag + GitHub Release
145
- npm version patch
146
- git push && git push --tags
170
+ Use Angular's `viewChildren` or `contentChildren` to collect the templates:
147
171
 
148
- # 4. Go to GitHub → Releases → "Draft a new release"
149
- # - Choose the tag you just pushed
150
- # - Publish the release
172
+ ```ts
173
+ @Component({...})
174
+ export class MyComponent {
175
+ // Use viewChildren when templates are defined inside this component
176
+ templates = viewChildren(TemplateSelection);
177
+
178
+ // Use contentChildren when accepting templates via content projection
179
+ // projectedTemplates = contentChildren(TemplateSelection);
180
+ }
181
+ ```
151
182
 
152
- # The publish workflow will run automatically and publish to npm
183
+ #### Full Example
153
184
 
154
- **Alternative**: You can also manually trigger the publish workflow from the GitHub repo → **Actions** tab → **"Publish to npmjs"** → **"Run workflow"**.
185
+ ```ts
186
+ import { Component, viewChildren, ViewContainerRef, inject, afterNextRender } from '@angular/core';
187
+ import { TemplateSelection } from '@tmjeee/w-lib';
188
+
189
+ @Component({
190
+ selector: 'my-component',
191
+ standalone: true,
192
+ imports: [TemplateSelection],
193
+ template: `
194
+ <ng-template templateSelection="header">
195
+ <h1>My Header</h1>
196
+ </ng-template>
197
+
198
+ <div *templateSelection="'content'">
199
+ Main content here
200
+ </div>
201
+ `
202
+ })
203
+ export class MyComponent {
204
+ private vcr = inject(ViewContainerRef);
205
+ templates = viewChildren(TemplateSelection);
206
+
207
+ constructor() {
208
+ afterNextRender(() => {
209
+ const headerTpl = this.templates()
210
+ .find(t => t.name() === 'header')
211
+ ?.templateRef;
212
+
213
+ if (headerTpl) {
214
+ this.vcr.createEmbeddedView(headerTpl);
215
+ }
216
+ });
217
+ }
218
+
219
+ getTemplate(name: string) {
220
+ return this.templates().find(t => t.name() === name)?.templateRef;
221
+ }
222
+ }
155
223
  ```
156
224
 
157
- ## Tooling
225
+ You can then use the retrieved `TemplateRef` with `*ngTemplateOutlet` or `ViewContainerRef.createEmbeddedView()`.
158
226
 
159
- - **Build**: [tsdown](https://tsdown.dev/) (Rolldown + Oxc)
160
- - **Testing**: [Vitest](https://vitest.dev/)
161
- - **TypeScript**: Strict mode, ES2022 target, bundler resolution
227
+ #### Notes
162
228
 
163
- ### Optional: Add oxlint + oxfmt
229
+ - The directive is **standalone**.
230
+ - Use `viewChildren(TemplateSelection)` for templates defined inside the component.
231
+ - Use `contentChildren(TemplateSelection)` when the templates are provided by the parent via content projection.
164
232
 
165
- For consistency with other `@tmjeee` projects you can add:
166
233
 
167
- ```bash
168
- npm install -D oxlint oxfmt
169
- ```
234
+ ## Disclaimer
170
235
 
171
- Then add scripts:
172
-
173
- ```json
174
- "lint": "oxlint",
175
- "format": "oxfmt --check",
176
- "format:fix": "oxfmt --write"
177
- ```
236
+ Disclaimer: Contains codes from [jyoung4242](https://github.com/jyoung4242/Game-Dev-Library).
178
237
 
179
238
  ## License
180
239
 
package/dist/index.d.ts CHANGED
@@ -1,17 +1,204 @@
1
- //#region src/index.d.ts
1
+ import * as _angular_core0 from "@angular/core";
2
+ import { Signal, TemplateRef } from "@angular/core";
3
+ import { Subject } from "rxjs";
4
+
5
+ //#region src/loading-state.d.ts
2
6
  /**
3
- * Classic hello world sample for the w-lib skeleton.
7
+ * A lightweight utility for managing per-action loading states using signals.
4
8
  *
5
- * @returns The canonical greeting string
9
+ * Philosophy:
10
+ * - Most errors should propagate to GlobalErrorHandler.
11
+ * - This utility helps manage UI loading states cleanly without forcing try/catch everywhere.
12
+ * - Use it when you need to disable buttons / show spinners during async actions.
13
+ *
14
+ * Recommended usage:
15
+ *
16
+ * const loading = createLoadingState<'creatingBoard' | 'deletingBoard'>();
17
+ *
18
+ * async createBoard() {
19
+ * const title = await openDialog();
20
+ * if (!title) return;
21
+ *
22
+ * await loading.withLoading('creatingBoard', async () => {
23
+ * const board = await this.boardService.createBoard(...);
24
+ * this.router.navigate(...);
25
+ * });
26
+ * }
27
+ *
28
+ * In template:
29
+ * <button [disabled]="loading.is('creatingBoard')()">
30
+ * {{ loading.is('creatingBoard')() ? 'Creating...' : 'Create Board' }}
31
+ * </button>
32
+ */
33
+ declare function createLoadingState<T extends string = string>(): {
34
+ is: (key: T) => Signal<boolean>;
35
+ set: (key: T, value: boolean) => void;
36
+ withLoading: <R>(key: T, fn: () => Promise<R>) => Promise<R>;
37
+ states: Signal<Partial<Record<T, boolean>>>;
38
+ };
39
+ /**
40
+ * Type helper for action loading keys.
41
+ * Example: type UserAction = 'addingToWorkspace' | 'removingFromWorkspace';
42
+ */
43
+ type LoadingKey = string;
44
+ //#endregion
45
+ //#region src/event-emitter.d.ts
46
+ interface EventEmitterHandler<T> {
47
+ (change: T): void;
48
+ }
49
+ interface EventEmitterSubscription {
50
+ unsubscribe: () => void;
51
+ }
52
+ interface EventEmitter<T> {
53
+ emit(event: string, payload: T): void;
54
+ on(event: string, handler: EventEmitterHandler<T>): void;
55
+ off(event: string, handler?: EventEmitterHandler<T>): void;
56
+ once(event: string, handler: EventEmitterHandler<T>): void;
57
+ }
58
+ declare class RxEventEmitter<T> implements EventEmitter<T> {
59
+ _listeners: Record<string, Subject<T>>;
60
+ emit(event: string, payload: T): void;
61
+ on(event: string, handler: EventEmitterHandler<T>): void;
62
+ off(event: string, handler?: EventEmitterHandler<T>): void;
63
+ once(event: string, handler: EventEmitterHandler<T>): void;
64
+ }
65
+ declare class JsEventEmitter<T> implements EventEmitter<T> {
66
+ _listeners: Record<string, EventEmitterHandler<T>[]>;
67
+ emit(event: string, payload: T): void;
68
+ on(event: string, handler: EventEmitterHandler<T>): void;
69
+ off(event: string, handler?: EventEmitterHandler<T>): void;
70
+ once(event: string, handler: EventEmitterHandler<T>): void;
71
+ }
72
+ //#endregion
73
+ //#region src/state-management.d.ts
74
+ type Primitive = string | number | boolean | null | undefined;
75
+ type PathsOf<T, Prefix extends string = ""> = T extends Primitive ? never : T extends Array<infer _> ? never : { [K in keyof T & string]: Prefix extends "" ? K | (T[K] extends Primitive ? never : PathsOf<T[K], K>) : `${Prefix}.${K}` | (T[K] extends Primitive ? never : PathsOf<T[K], `${Prefix}.${K}`>) }[keyof T & string];
76
+ type Path<T> = PathsOf<T>;
77
+ type PathValue<T, P extends string> = P extends keyof T ? T[P] : P extends `${infer K}.${infer Rest}` ? K extends keyof T ? Rest extends Path<T[K]> ? PathValue<T[K], Rest> : never : never : never;
78
+ interface ChangePayload<State, V = unknown> {
79
+ path: string;
80
+ previousValue: V;
81
+ value: V;
82
+ previousState: Readonly<State>;
83
+ state: Readonly<State>;
84
+ }
85
+ type ChangeEventMap<State> = {
86
+ change: ChangePayload<State>;
87
+ [key: `change:${string}`]: ChangePayload<State>;
88
+ };
89
+ interface StateStore<State extends object> {
90
+ get(): Readonly<State>;
91
+ get<K extends Path<State>>(path: K): Readonly<PathValue<State, K>>;
92
+ select<T>(selector: (state: Readonly<State>) => T): Readonly<T>;
93
+ set<K extends Path<State>>(path: K, value: PathValue<State, K>): void;
94
+ update<K extends Path<State>>(path: K, fn: (current: PathValue<State, K>) => PathValue<State, K>): void;
95
+ patch(partial: DeepPartial<State>): void;
96
+ batch(fn: (store: BatchContext<State>) => void): void;
97
+ on<E extends keyof ChangeEventMap<State>>(event: E, handler: (payload: ChangeEventMap<State>[E]) => void): void;
98
+ off<E extends keyof ChangeEventMap<State>>(event: E, handler: (payload: ChangeEventMap<State>[E]) => void): void;
99
+ subscribe<K extends Path<State>>(path: K, handler: (payload: ChangePayload<State, PathValue<State, K>>) => void): () => void;
100
+ serialize(): string;
101
+ reset(): void;
102
+ }
103
+ interface BatchContext<State extends object> {
104
+ set<K extends Path<State>>(path: K, value: PathValue<State, K>): void;
105
+ update<K extends Path<State>>(path: K, fn: (current: PathValue<State, K>) => PathValue<State, K>): void;
106
+ patch(partial: DeepPartial<State>): void;
107
+ }
108
+ type DeepPartial<T> = T extends Primitive ? T : { [K in keyof T]?: DeepPartial<T[K]> };
109
+ declare function createStateStore<State extends object>(initialState: State, eventEmitter?: EventEmitter<ChangePayload<State>>): StateStore<State>;
110
+ //#endregion
111
+ //#region src/finite-state-machine.d.ts
112
+ type FsmResult<T = void> = {
113
+ success: boolean;
114
+ message?: string;
115
+ value?: T;
116
+ };
117
+ declare class Fsm {
118
+ states: Map<string, FsmState>;
119
+ current: FsmState | null;
120
+ currentParams: any[];
121
+ currentTime: number;
122
+ constructor();
123
+ /** Register new states, returns array of registered ExState */
124
+ register(...types: (string | FsmState)[]): FsmResult<FsmState[]>;
125
+ /** Change current state */
126
+ set(state: string | FsmState, ...params: any[]): FsmResult<FsmState> | Promise<FsmResult<FsmState>>;
127
+ /** Check if a state is registered */
128
+ has(state: string | FsmState): boolean;
129
+ /** Get current state */
130
+ get(): FsmResult<FsmState>;
131
+ /** Reset FSM */
132
+ reset(): FsmResult<void>;
133
+ /** Update current state */
134
+ update(): FsmResult<void> | Promise<FsmResult<void>>;
135
+ }
136
+ declare class FsmState {
137
+ name: string;
138
+ constructor(name: string);
139
+ enter(_previous: FsmState | null, ..._params: any): void | Promise<void>;
140
+ exit(_next: FsmState | null, ..._params: any): void | Promise<void>;
141
+ update(..._params: any): void | Promise<void>;
142
+ }
143
+ //#endregion
144
+ //#region src/template-selection.d.ts
145
+ /**
146
+ * A directive that marks a template (or element) with a name so it can be
147
+ * queried and rendered manually.
148
+ *
149
+ * This is useful in two main scenarios:
150
+ * - Defining multiple named templates inside a component and choosing which one to render at runtime.
151
+ * - Accepting named templates from a parent component via content projection.
152
+ *
153
+ * ## Supported Syntaxes
154
+ *
155
+ * ```html
156
+ * <!-- On ng-template (recommended for pure templates) -->
157
+ * <ng-template templateSelection="header">...</ng-template>
158
+ * <ng-template templateSelection="footer">...</ng-template>
159
+ *
160
+ * <!-- Using structural directive syntax (also supported) -->
161
+ * <div *templateSelection="'sidebar'">Sidebar content</div>
162
+ * ```
163
+ *
164
+ * ## Consumption Example
165
+ *
166
+ * You can query the templates using either `viewChildren()` or `contentChildren()`,
167
+ * depending on your use case:
6
168
  *
7
- * @example
8
169
  * ```ts
9
- * import { helloWorld } from '@tmjeee/w-lib';
170
+ * @Component({...})
171
+ * export class MyComponent {
172
+ * // Use viewChildren() if the templates are defined inside this component's own template
173
+ * internalTemplates = viewChildren(TemplateSelection);
174
+ *
175
+ * // Use contentChildren() if you want to accept named templates from the parent via content projection
176
+ * projectedTemplates = contentChildren(TemplateSelection);
177
+ *
178
+ * getTemplate(name: string): TemplateRef<any> | undefined {
179
+ * return this.internalTemplates()
180
+ * .find(t => t.name() === name)
181
+ * ?.templateRef;
182
+ * }
183
+ * }
184
+ * ```
10
185
  *
11
- * console.log(helloWorld()); // "Hello, world!"
186
+ * Then in the template:
187
+ * ```html
188
+ * <ng-container *ngTemplateOutlet="getTemplate('header')"></ng-container>
12
189
  * ```
13
190
  */
14
- declare function helloWorld(): string;
191
+ declare class TemplateSelection {
192
+ /**
193
+ * The name used to identify this template.
194
+ * Can be bound as `templateSelection="myName"` or `[templateSelection]="someVariable"`.
195
+ */
196
+ name: _angular_core0.InputSignal<string>;
197
+ /**
198
+ * The underlying TemplateRef that can be used with *ngTemplateOutlet or ViewContainerRef.
199
+ */
200
+ readonly templateRef: TemplateRef<any>;
201
+ }
15
202
  //#endregion
16
- export { helloWorld };
203
+ export { BatchContext, ChangePayload, EventEmitter, EventEmitterHandler, EventEmitterSubscription, Fsm, FsmResult, FsmState, JsEventEmitter, LoadingKey, Path, PathValue, RxEventEmitter, StateStore, TemplateSelection, createLoadingState, createStateStore };
17
204
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";;AAYA;;;;;;;;;;;iBAAgB,UAAA,CAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/loading-state.ts","../src/event-emitter.ts","../src/state-management.ts","../src/finite-state-machine.ts","../src/template-selection.ts"],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1,20 +1,445 @@
1
- //#region src/index.ts
1
+ import { Directive, TemplateRef, computed, inject, input, signal } from "@angular/core";
2
+ import { Subject } from "rxjs";
3
+ import { take } from "rxjs/operators";
4
+
5
+ //#region src/loading-state.ts
2
6
  /**
3
- * Classic hello world sample for the w-lib skeleton.
7
+ * A lightweight utility for managing per-action loading states using signals.
8
+ *
9
+ * Philosophy:
10
+ * - Most errors should propagate to GlobalErrorHandler.
11
+ * - This utility helps manage UI loading states cleanly without forcing try/catch everywhere.
12
+ * - Use it when you need to disable buttons / show spinners during async actions.
13
+ *
14
+ * Recommended usage:
4
15
  *
5
- * @returns The canonical greeting string
16
+ * const loading = createLoadingState<'creatingBoard' | 'deletingBoard'>();
6
17
  *
7
- * @example
8
- * ```ts
9
- * import { helloWorld } from '@tmjeee/w-lib';
18
+ * async createBoard() {
19
+ * const title = await openDialog();
20
+ * if (!title) return;
10
21
  *
11
- * console.log(helloWorld()); // "Hello, world!"
12
- * ```
22
+ * await loading.withLoading('creatingBoard', async () => {
23
+ * const board = await this.boardService.createBoard(...);
24
+ * this.router.navigate(...);
25
+ * });
26
+ * }
27
+ *
28
+ * In template:
29
+ * <button [disabled]="loading.is('creatingBoard')()">
30
+ * {{ loading.is('creatingBoard')() ? 'Creating...' : 'Create Board' }}
31
+ * </button>
13
32
  */
14
- function helloWorld() {
15
- return "Hello, world!";
33
+ function createLoadingState() {
34
+ const loadingMap = signal({});
35
+ const signalCache = /* @__PURE__ */ new Map();
36
+ /**
37
+ * Returns a reactive signal indicating if a specific action is loading.
38
+ * The returned signal is stable (same reference) for the same key.
39
+ */
40
+ const is = (key) => {
41
+ if (!signalCache.has(key)) signalCache.set(key, computed(() => loadingMap()[key] ?? false));
42
+ return signalCache.get(key);
43
+ };
44
+ /**
45
+ * Manually set the loading state for a key.
46
+ */
47
+ const set = (key, value) => {
48
+ loadingMap.update((map) => ({
49
+ ...map,
50
+ [key]: value
51
+ }));
52
+ };
53
+ /**
54
+ * Runs an async function while automatically managing the loading state.
55
+ * Errors are allowed to propagate (as per project convention).
56
+ */
57
+ const withLoading = async (key, fn) => {
58
+ set(key, true);
59
+ try {
60
+ return await fn();
61
+ } finally {
62
+ set(key, false);
63
+ }
64
+ };
65
+ return {
66
+ is,
67
+ set,
68
+ withLoading,
69
+ states: loadingMap.asReadonly()
70
+ };
71
+ }
72
+
73
+ //#endregion
74
+ //#region src/event-emitter.ts
75
+ var RxEventEmitter = class {
76
+ _listeners = {};
77
+ emit(event, payload) {
78
+ const handlers = this._listeners[event];
79
+ if (handlers) handlers.next(payload);
80
+ }
81
+ on(event, handler) {
82
+ if (!this._listeners[event]) this._listeners[event] = new Subject();
83
+ this._listeners[event].subscribe(handler);
84
+ }
85
+ off(event, handler) {
86
+ if (this._listeners[event]) {
87
+ this._listeners[event].complete();
88
+ delete this._listeners[event];
89
+ }
90
+ }
91
+ once(event, handler) {
92
+ if (!this._listeners[event]) this._listeners[event] = new Subject();
93
+ this._listeners[event].pipe(take(1)).subscribe(handler);
94
+ }
95
+ };
96
+ var JsEventEmitter = class {
97
+ _listeners = {};
98
+ emit(event, payload) {
99
+ const handlers = this._listeners[event];
100
+ if (handlers && handlers.length) handlers.forEach((h) => {
101
+ h(payload);
102
+ });
103
+ }
104
+ on(event, handler) {
105
+ if (!this._listeners[event]) this._listeners[event] = [];
106
+ if (!this._listeners[event].find((h) => h == handler)) this._listeners[event].push(handler);
107
+ }
108
+ off(event, handler) {
109
+ if (!this._listeners[event]) this._listeners[event] = [];
110
+ if (handler) {
111
+ const idx = this._listeners[event].findIndex((h) => h == handler);
112
+ if (idx >= 0) this._listeners[event].splice(idx, 1);
113
+ } else delete this._listeners[event];
114
+ }
115
+ once(event, handler) {
116
+ if (!this._listeners[event]) this._listeners[event] = [];
117
+ const onceOnlyHandler = (evt) => {
118
+ try {
119
+ handler(evt);
120
+ } finally {
121
+ this.off(event, onceOnlyHandler);
122
+ }
123
+ };
124
+ this._listeners[event].push(onceOnlyHandler);
125
+ }
126
+ };
127
+
128
+ //#endregion
129
+ //#region src/state-management.ts
130
+ function parsePath(path) {
131
+ return path.split(".");
132
+ }
133
+ function deepFreeze(obj) {
134
+ if (obj === null || typeof obj !== "object") return obj;
135
+ Object.getOwnPropertyNames(obj).forEach((name) => {
136
+ const val = obj[name];
137
+ if (val && typeof val === "object") deepFreeze(val);
138
+ });
139
+ return Object.freeze(obj);
16
140
  }
141
+ function deepClone(obj) {
142
+ if (obj === null || typeof obj !== "object") return obj;
143
+ if (Array.isArray(obj)) return obj.map(deepClone);
144
+ const result = {};
145
+ for (const key of Object.keys(obj)) result[key] = deepClone(obj[key]);
146
+ return result;
147
+ }
148
+ function getAtPath(obj, segments) {
149
+ let current = obj;
150
+ for (const seg of segments) {
151
+ if (current === null || typeof current !== "object") throw new Error(`Invalid path segment "${seg}": not an object`);
152
+ current = current[seg];
153
+ }
154
+ return current;
155
+ }
156
+ function setAtPath(obj, segments, value) {
157
+ if (segments.length === 0) return value;
158
+ const [head, ...tail] = segments;
159
+ if (typeof obj !== "object" || obj === null) throw new Error(`Cannot set path on non-object at segment "${head}"`);
160
+ const record = obj;
161
+ const updated = { ...record };
162
+ if (tail.length === 0) updated[head] = value;
163
+ else {
164
+ const child = record[head];
165
+ if (child === null || typeof child !== "object") throw new Error(`Path segment "${head}" is not an object`);
166
+ updated[head] = setAtPath(child, tail, value);
167
+ }
168
+ return updated;
169
+ }
170
+ function shallowEqual(a, b) {
171
+ if (a === b) return true;
172
+ if (a === null || b === null) return false;
173
+ if (typeof a !== "object" || typeof b !== "object") return false;
174
+ const keysA = Object.keys(a);
175
+ const keysB = Object.keys(b);
176
+ if (keysA.length !== keysB.length) return false;
177
+ for (const k of keysA) if (a[k] !== b[k]) return false;
178
+ return true;
179
+ }
180
+ function deepMerge(base, partial) {
181
+ const result = { ...base };
182
+ for (const key of Object.keys(partial)) {
183
+ const pVal = partial[key];
184
+ const bVal = base[key];
185
+ if (pVal !== void 0 && pVal !== null && typeof pVal === "object" && !Array.isArray(pVal) && bVal !== null && typeof bVal === "object") result[key] = deepMerge(bVal, pVal);
186
+ else if (pVal !== void 0) result[key] = pVal;
187
+ }
188
+ return result;
189
+ }
190
+ function createStateStore(initialState, eventEmitter = new JsEventEmitter()) {
191
+ const _initial = deepFreeze(deepClone(initialState));
192
+ let _state = _initial;
193
+ const _emitter = eventEmitter;
194
+ function _emit(path, previousValue, value, previousState) {
195
+ const payload = {
196
+ path,
197
+ previousValue,
198
+ value,
199
+ previousState,
200
+ state: _state
201
+ };
202
+ _emitter.emit("change", payload);
203
+ _emitter.emit(`change:${path}`, payload);
204
+ }
205
+ function _applySet(currentState, segments, value) {
206
+ return deepFreeze(setAtPath(deepClone(currentState), segments, value));
207
+ }
208
+ function get(path) {
209
+ if (path === void 0) return _state;
210
+ const segments = parsePath(path);
211
+ return deepFreeze(deepClone(getAtPath(_state, segments)));
212
+ }
213
+ function select(selector) {
214
+ const result = selector(_state);
215
+ if (result !== null && typeof result === "object") return deepFreeze(deepClone(result));
216
+ return result;
217
+ }
218
+ function set(path, value) {
219
+ const segments = parsePath(path);
220
+ const previousValue = getAtPath(_state, segments);
221
+ if (shallowEqual(previousValue, value)) return;
222
+ const previousState = _state;
223
+ _state = _applySet(_state, segments, value);
224
+ _emit(path, previousValue, value, previousState);
225
+ }
226
+ function update(path, fn) {
227
+ const segments = parsePath(path);
228
+ set(path, fn(deepClone(getAtPath(_state, segments))));
229
+ }
230
+ function patch(partial) {
231
+ const nextState = deepFreeze(deepMerge(_state, partial));
232
+ const changedPaths = [];
233
+ collectChangedPaths("", _state, nextState, changedPaths);
234
+ if (changedPaths.length === 0) return;
235
+ const previousState = _state;
236
+ _state = nextState;
237
+ for (const entry of changedPaths) _emit(entry.path, entry.prev, entry.next, previousState);
238
+ }
239
+ function collectChangedPaths(prefix, prev, next, out) {
240
+ if (prev === next) return;
241
+ if (prev !== null && next !== null && typeof prev === "object" && typeof next === "object" && !Array.isArray(prev) && !Array.isArray(next)) {
242
+ const allKeys = new Set([...Object.keys(prev), ...Object.keys(next)]);
243
+ for (const k of allKeys) {
244
+ const pv = prev[k];
245
+ const nv = next[k];
246
+ collectChangedPaths(prefix ? `${prefix}.${k}` : k, pv, nv, out);
247
+ }
248
+ } else if (!shallowEqual(prev, next)) out.push({
249
+ path: prefix,
250
+ prev,
251
+ next
252
+ });
253
+ }
254
+ function batch(fn) {
255
+ let pendingState = _state;
256
+ const changes = [];
257
+ const ctx = {
258
+ set(path, value) {
259
+ const segments = parsePath(path);
260
+ const previousValue = getAtPath(pendingState, segments);
261
+ if (shallowEqual(previousValue, value)) return;
262
+ const prevState = pendingState;
263
+ pendingState = deepFreeze(setAtPath(deepClone(pendingState), segments, value));
264
+ changes.push({
265
+ path,
266
+ prev: previousValue,
267
+ next: value,
268
+ prevState
269
+ });
270
+ },
271
+ update(path, fn2) {
272
+ const segments = parsePath(path);
273
+ const next = fn2(deepClone(getAtPath(pendingState, segments)));
274
+ ctx.set(path, next);
275
+ },
276
+ patch(partial) {
277
+ const merged = deepFreeze(deepMerge(pendingState, partial));
278
+ const changedPaths = [];
279
+ collectChangedPaths("", pendingState, merged, changedPaths);
280
+ if (changedPaths.length === 0) return;
281
+ const prevState = pendingState;
282
+ pendingState = merged;
283
+ for (const entry of changedPaths) changes.push({
284
+ path: entry.path,
285
+ prev: entry.prev,
286
+ next: entry.next,
287
+ prevState
288
+ });
289
+ }
290
+ };
291
+ fn(ctx);
292
+ if (pendingState === _state) return;
293
+ _state = pendingState;
294
+ for (const change of changes) _emit(change.path, change.prev, change.next, change.prevState);
295
+ }
296
+ function on(event, handler) {
297
+ _emitter.on(event, handler);
298
+ }
299
+ function off(event, handler) {
300
+ _emitter.off(event, handler);
301
+ }
302
+ function subscribe(path, handler) {
303
+ const eventName = `change:${path}`;
304
+ const wrapped = (payload) => handler(payload);
305
+ _emitter.on(eventName, wrapped);
306
+ return () => _emitter.off(eventName, wrapped);
307
+ }
308
+ function serialize() {
309
+ return JSON.stringify(_state);
310
+ }
311
+ function reset() {
312
+ const previousState = _state;
313
+ _state = _initial;
314
+ const changes = [];
315
+ collectChangedPaths("", previousState, _state, changes);
316
+ for (const change of changes) _emit(change.path, change.prev, change.next, previousState);
317
+ }
318
+ return {
319
+ get,
320
+ select,
321
+ set,
322
+ update,
323
+ patch,
324
+ batch,
325
+ on,
326
+ off,
327
+ subscribe,
328
+ serialize,
329
+ reset
330
+ };
331
+ }
332
+
333
+ //#endregion
334
+ //#region src/finite-state-machine.ts
335
+ var Fsm = class {
336
+ states = /* @__PURE__ */ new Map();
337
+ current = null;
338
+ currentParams = [];
339
+ currentTime = 0;
340
+ constructor() {}
341
+ /** Register new states, returns array of registered ExState */
342
+ register(...types) {
343
+ const generatedStates = [];
344
+ for (const state of types) {
345
+ const newState = typeof state === "string" ? new FsmState(state) : state;
346
+ if (this.states.has(newState.name)) console.warn(`State "${newState.name}" is already registered and will be overwritten.`);
347
+ this.states.set(newState.name, newState);
348
+ generatedStates.push(newState);
349
+ }
350
+ return {
351
+ success: true,
352
+ value: generatedStates
353
+ };
354
+ }
355
+ /** Change current state */
356
+ set(state, ...params) {
357
+ const name = typeof state === "string" ? state : state.name;
358
+ const next = this.states.get(name);
359
+ if (!next) return {
360
+ success: false,
361
+ message: `State "${name}" not found`
362
+ };
363
+ const finalize = () => {
364
+ this.current = next;
365
+ this.currentParams = params;
366
+ this.currentTime = Date.now();
367
+ return {
368
+ success: true,
369
+ value: next
370
+ };
371
+ };
372
+ if (!this.current) {
373
+ const entering = next.enter(this.current, ...params);
374
+ return entering instanceof Promise ? entering.then(finalize) : finalize();
375
+ }
376
+ const leaving = this.current.exit(next, ...params);
377
+ if (leaving instanceof Promise) return leaving.then(() => {
378
+ const entering = next.enter(this.current, ...params);
379
+ return entering instanceof Promise ? entering.then(finalize) : finalize();
380
+ });
381
+ const entering = next.enter(this.current, ...params);
382
+ return entering instanceof Promise ? entering.then(finalize) : finalize();
383
+ }
384
+ /** Check if a state is registered */
385
+ has(state) {
386
+ const name = typeof state === "string" ? state : state.name;
387
+ return this.states.has(name);
388
+ }
389
+ /** Get current state */
390
+ get() {
391
+ if (!this.current) return {
392
+ success: false,
393
+ message: "No state set"
394
+ };
395
+ return {
396
+ success: true,
397
+ value: this.current
398
+ };
399
+ }
400
+ /** Reset FSM */
401
+ reset() {
402
+ this.current = null;
403
+ this.currentParams = [];
404
+ this.currentTime = 0;
405
+ return { success: true };
406
+ }
407
+ /** Update current state */
408
+ update() {
409
+ if (!this.current) return {
410
+ success: false,
411
+ message: "No current state"
412
+ };
413
+ const result = this.current.update(...this.currentParams);
414
+ return result instanceof Promise ? result.then(() => ({ success: true })) : { success: true };
415
+ }
416
+ };
417
+ var FsmState = class {
418
+ constructor(name) {
419
+ this.name = name;
420
+ }
421
+ enter(_previous, ..._params) {}
422
+ exit(_next, ..._params) {}
423
+ update(..._params) {}
424
+ };
425
+
426
+ //#endregion
427
+ //#region src/template-selection.ts
428
+ var TemplateSelection = @Directive({
429
+ selector: "[templateSelection]",
430
+ standalone: true
431
+ }) class {
432
+ /**
433
+ * The name used to identify this template.
434
+ * Can be bound as `templateSelection="myName"` or `[templateSelection]="someVariable"`.
435
+ */
436
+ name = input.required({ alias: "templateSelection" });
437
+ /**
438
+ * The underlying TemplateRef that can be used with *ngTemplateOutlet or ViewContainerRef.
439
+ */
440
+ templateRef = inject(TemplateRef);
441
+ };
17
442
 
18
443
  //#endregion
19
- export { helloWorld };
444
+ export { Fsm, FsmState, JsEventEmitter, RxEventEmitter, TemplateSelection, createLoadingState, createStateStore };
20
445
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * Classic hello world sample for the w-lib skeleton.\n *\n * @returns The canonical greeting string\n *\n * @example\n * ```ts\n * import { helloWorld } from '@tmjeee/w-lib';\n *\n * console.log(helloWorld()); // \"Hello, world!\"\n * ```\n */\nexport function helloWorld(): string {\n return 'Hello, world!';\n}\n"],"mappings":";;;;;;;;;;;;;AAYA,SAAgB,aAAqB;AACnC,QAAO"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/loading-state.ts","../src/event-emitter.ts","../src/state-management.ts","../src/finite-state-machine.ts","../src/template-selection.ts"],"sourcesContent":["import { computed, signal, Signal } from '@angular/core';\n\n/**\n * A lightweight utility for managing per-action loading states using signals.\n *\n * Philosophy:\n * - Most errors should propagate to GlobalErrorHandler.\n * - This utility helps manage UI loading states cleanly without forcing try/catch everywhere.\n * - Use it when you need to disable buttons / show spinners during async actions.\n *\n * Recommended usage:\n *\n * const loading = createLoadingState<'creatingBoard' | 'deletingBoard'>();\n *\n * async createBoard() {\n * const title = await openDialog();\n * if (!title) return;\n *\n * await loading.withLoading('creatingBoard', async () => {\n * const board = await this.boardService.createBoard(...);\n * this.router.navigate(...);\n * });\n * }\n *\n * In template:\n * <button [disabled]=\"loading.is('creatingBoard')()\">\n * {{ loading.is('creatingBoard')() ? 'Creating...' : 'Create Board' }}\n * </button>\n */\nexport function createLoadingState<T extends string = string>() {\n const loadingMap = signal<Partial<Record<T, boolean>>>({});\n const signalCache = new Map<T, Signal<boolean>>();\n\n /**\n * Returns a reactive signal indicating if a specific action is loading.\n * The returned signal is stable (same reference) for the same key.\n */\n const is = (key: T): Signal<boolean> => {\n if (!signalCache.has(key)) {\n signalCache.set(\n key,\n computed(() => loadingMap()[key] ?? false)\n );\n }\n return signalCache.get(key)!;\n };\n\n /**\n * Manually set the loading state for a key.\n */\n const set = (key: T, value: boolean): void => {\n loadingMap.update((map) => ({ ...map, [key]: value }));\n };\n\n /**\n * Runs an async function while automatically managing the loading state.\n * Errors are allowed to propagate (as per project convention).\n */\n const withLoading = async <R>(key: T, fn: () => Promise<R>): Promise<R> => {\n set(key, true);\n try {\n return await fn();\n } finally {\n set(key, false);\n }\n };\n\n /**\n * Raw access to the loading map (rarely needed).\n */\n const states = loadingMap.asReadonly();\n\n return {\n is,\n set,\n withLoading,\n states,\n };\n}\n\n/**\n * Type helper for action loading keys.\n * Example: type UserAction = 'addingToWorkspace' | 'removingFromWorkspace';\n */\nexport type LoadingKey = string;\n","\nexport interface EventEmitterHandler<T> {\n (change:T):void;\n}\n\nexport interface EventEmitterSubscription {\n unsubscribe:()=>void;\n}\n\n\nexport interface EventEmitter<T> {\n emit(event: string, payload: T): void;\n on(event: string, handler: EventEmitterHandler<T>): void;\n off(event: string, handler?: EventEmitterHandler<T>): void;\n once(event: string, handler: EventEmitterHandler<T>): void;\n}\n\n// =============================================================\n// ============== RxJs =========================================\n// =============================================================\n\nimport {Subject} from 'rxjs';\nimport {take} from 'rxjs/operators';\n\nexport class RxEventEmitter<T> implements EventEmitter<T> {\n\n _listeners: Record<string, Subject<T>> = {};\n\n emit(event: string, payload: T): void {\n const handlers = this._listeners[event];\n if (handlers) {\n handlers.next(payload);\n }\n }\n on(event: string, handler: EventEmitterHandler<T>): void {\n if (!this._listeners[event]) {\n this._listeners[event] = new Subject();\n }\n this._listeners[event].subscribe(handler)\n }\n off(event: string, handler?: EventEmitterHandler<T>): void {\n const s = this._listeners[event];\n if (s) {\n this._listeners[event].complete();\n delete this._listeners[event];\n }\n }\n once(event: string, handler: EventEmitterHandler<T>): void {\n if (!this._listeners[event]) {\n this._listeners[event] = new Subject();\n }\n this._listeners[event].pipe(\n take(1)\n ).subscribe(handler);\n }\n}\n\n\n// =============================================================\n// ============== Js ===========================================\n// =============================================================\n\nexport class JsEventEmitter<T> implements EventEmitter<T> {\n\n _listeners: Record<string, EventEmitterHandler<T>[]> = {};\n\n emit(event: string, payload: T): void {\n const handlers = this._listeners[event];\n if (handlers && handlers.length) {\n handlers.forEach(h => {\n h(payload);\n })\n }\n }\n on(event: string, handler: EventEmitterHandler<T>): void {\n if (!this._listeners[event]) {\n this._listeners[event] = [];\n }\n const exists = this._listeners[event].find(h => h == handler);\n if (!exists) {\n this._listeners[event].push(handler);\n }\n }\n off(event: string, handler?: EventEmitterHandler<T>): void {\n if (!this._listeners[event]) {\n this._listeners[event] = [];\n }\n if (handler) {\n const idx = this._listeners[event].findIndex(h => h == handler);\n if (idx >= 0) {\n this._listeners[event].splice(idx, 1);\n }\n } else {\n delete this._listeners[event];\n }\n }\n once(event: string, handler:EventEmitterHandler<T>): void {\n if (!this._listeners[event]) {\n this._listeners[event] = [];\n }\n const onceOnlyHandler = (evt: T) => {\n try {\n handler(evt);\n } finally {\n this.off(event, onceOnlyHandler);\n }\n };\n this._listeners[event].push(onceOnlyHandler);\n }\n}","// ─── Path Typing ────────────────────────────────────────────────────────────\nimport {EventEmitter, JsEventEmitter} from './event-emitter';\n\ntype Primitive = string | number | boolean | null | undefined;\n\ntype PathsOf<T, Prefix extends string = \"\"> = T extends Primitive\n ? never\n : T extends Array<infer _>\n ? never\n : {\n [K in keyof T & string]: Prefix extends \"\"\n ? K | (T[K] extends Primitive ? never : PathsOf<T[K], K>)\n : `${Prefix}.${K}` | (T[K] extends Primitive ? never : PathsOf<T[K], `${Prefix}.${K}`>);\n }[keyof T & string];\n\nexport type Path<T> = PathsOf<T>;\n\nexport type PathValue<T, P extends string> = P extends keyof T\n ? T[P]\n : P extends `${infer K}.${infer Rest}`\n ? K extends keyof T\n ? Rest extends Path<T[K]>\n ? PathValue<T[K], Rest>\n : never\n : never\n : never;\n\n// ─── Event Types ─────────────────────────────────────────────────────────────\n\nexport interface ChangePayload<State, V = unknown> {\n path: string;\n previousValue: V;\n value: V;\n previousState: Readonly<State>;\n state: Readonly<State>;\n}\n\ntype ChangeEventMap<State> = {\n change: ChangePayload<State>;\n [key: `change:${string}`]: ChangePayload<State>;\n};\n\n// ─── StateStore Interface ────────────────────────────────────────────────────\n\nexport interface StateStore<State extends object> {\n get(): Readonly<State>;\n get<K extends Path<State>>(path: K): Readonly<PathValue<State, K>>;\n select<T>(selector: (state: Readonly<State>) => T): Readonly<T>;\n\n set<K extends Path<State>>(path: K, value: PathValue<State, K>): void;\n update<K extends Path<State>>(path: K, fn: (current: PathValue<State, K>) => PathValue<State, K>): void;\n patch(partial: DeepPartial<State>): void;\n batch(fn: (store: BatchContext<State>) => void): void;\n\n on<E extends keyof ChangeEventMap<State>>(event: E, handler: (payload: ChangeEventMap<State>[E]) => void): void;\n off<E extends keyof ChangeEventMap<State>>(event: E, handler: (payload: ChangeEventMap<State>[E]) => void): void;\n\n subscribe<K extends Path<State>>(path: K, handler: (payload: ChangePayload<State, PathValue<State, K>>) => void): () => void;\n\n serialize(): string;\n reset(): void;\n}\n\nexport interface BatchContext<State extends object> {\n set<K extends Path<State>>(path: K, value: PathValue<State, K>): void;\n update<K extends Path<State>>(path: K, fn: (current: PathValue<State, K>) => PathValue<State, K>): void;\n patch(partial: DeepPartial<State>): void;\n}\n\ntype DeepPartial<T> = T extends Primitive ? T : { [K in keyof T]?: DeepPartial<T[K]> };\n\n// ─── Utility Helpers ─────────────────────────────────────────────────────────\n\nfunction parsePath(path: string): string[] {\n return path.split(\".\");\n}\n\nfunction deepFreeze<T>(obj: T): Readonly<T> {\n if (obj === null || typeof obj !== \"object\") return obj;\n Object.getOwnPropertyNames(obj).forEach(name => {\n const val = (obj as Record<string, unknown>)[name];\n if (val && typeof val === \"object\") deepFreeze(val);\n });\n return Object.freeze(obj);\n}\n\nfunction deepClone<T>(obj: T): T {\n if (obj === null || typeof obj !== \"object\") return obj;\n if (Array.isArray(obj)) return (obj as unknown[]).map(deepClone) as unknown as T;\n const result = {} as Record<string, unknown>;\n for (const key of Object.keys(obj as object)) {\n result[key] = deepClone((obj as Record<string, unknown>)[key]);\n }\n return result as T;\n}\n\nfunction getAtPath<T>(obj: T, segments: string[]): unknown {\n let current: unknown = obj;\n for (const seg of segments) {\n if (current === null || typeof current !== \"object\") {\n throw new Error(`Invalid path segment \"${seg}\": not an object`);\n }\n current = (current as Record<string, unknown>)[seg];\n }\n return current;\n}\n\nfunction setAtPath<T extends object>(obj: T, segments: string[], value: unknown): T {\n if (segments.length === 0) return value as T;\n const [head, ...tail] = segments;\n if (typeof obj !== \"object\" || obj === null) {\n throw new Error(`Cannot set path on non-object at segment \"${head}\"`);\n }\n const record = obj as Record<string, unknown>;\n const updated = { ...record };\n if (tail.length === 0) {\n updated[head] = value;\n } else {\n const child = record[head];\n if (child === null || typeof child !== \"object\") {\n throw new Error(`Path segment \"${head}\" is not an object`);\n }\n updated[head] = setAtPath(child as object, tail, value);\n }\n return updated as T;\n}\n\nfunction shallowEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (a === null || b === null) return false;\n if (typeof a !== \"object\" || typeof b !== \"object\") return false;\n const keysA = Object.keys(a as object);\n const keysB = Object.keys(b as object);\n if (keysA.length !== keysB.length) return false;\n for (const k of keysA) {\n if ((a as Record<string, unknown>)[k] !== (b as Record<string, unknown>)[k]) return false;\n }\n return true;\n}\n\nfunction deepMerge<T extends object>(base: T, partial: DeepPartial<T>): T {\n const result = { ...base } as Record<string, unknown>;\n for (const key of Object.keys(partial as object)) {\n const pVal = (partial as Record<string, unknown>)[key];\n const bVal = (base as Record<string, unknown>)[key];\n if (\n pVal !== undefined &&\n pVal !== null &&\n typeof pVal === \"object\" &&\n !Array.isArray(pVal) &&\n bVal !== null &&\n typeof bVal === \"object\"\n ) {\n result[key] = deepMerge(bVal as object, pVal as DeepPartial<object>);\n } else if (pVal !== undefined) {\n result[key] = pVal;\n }\n }\n return result as T;\n}\n\n// ─── Factory ─────────────────────────────────────────────────────────────────\n\nexport function createStateStore<State extends object>(\n initialState: State, \n eventEmitter: EventEmitter<ChangePayload<State>> = new JsEventEmitter<ChangePayload<State>>()\n): StateStore<State> {\n\n const _initial: Readonly<State> = deepFreeze(deepClone(initialState));\n let _state: Readonly<State> = _initial;\n\n const _emitter = eventEmitter;\n\n function _emit(path: string, previousValue: unknown, value: unknown, previousState: Readonly<State>): void {\n const payload: ChangePayload<State> = {\n path,\n previousValue,\n value,\n previousState,\n state: _state,\n };\n _emitter.emit(\"change\", payload);\n _emitter.emit(`change:${path}` as `change:${string}`, payload);\n }\n\n function _applySet(currentState: Readonly<State>, segments: string[], value: unknown): Readonly<State> {\n return deepFreeze(setAtPath(deepClone(currentState), segments, value));\n }\n\n // Overloaded get\n function get(): Readonly<State>;\n function get<K extends Path<State>>(path: K): Readonly<PathValue<State, K>>;\n function get<K extends Path<State>>(path?: K): Readonly<State> | Readonly<PathValue<State, K>> {\n if (path === undefined) return _state;\n const segments = parsePath(path);\n const val = getAtPath(_state, segments);\n return deepFreeze(deepClone(val)) as Readonly<PathValue<State, K>>;\n }\n\n function select<T>(selector: (state: Readonly<State>) => T): Readonly<T> {\n const result = selector(_state);\n if (result !== null && typeof result === \"object\") {\n return deepFreeze(deepClone(result)) as Readonly<T>;\n }\n return result as Readonly<T>;\n }\n\n function set<K extends Path<State>>(path: K, value: PathValue<State, K>): void {\n const segments = parsePath(path);\n const previousValue = getAtPath(_state, segments);\n if (shallowEqual(previousValue, value)) return;\n const previousState = _state;\n _state = _applySet(_state, segments, value);\n _emit(path, previousValue, value, previousState);\n }\n\n function update<K extends Path<State>>(path: K, fn: (current: PathValue<State, K>) => PathValue<State, K>): void {\n const segments = parsePath(path);\n const current = getAtPath(_state, segments) as PathValue<State, K>;\n const next = fn(deepClone(current));\n set(path, next);\n }\n\n function patch(partial: DeepPartial<State>): void {\n const merged = deepMerge(_state as State, partial);\n const nextState = deepFreeze(merged);\n const changedPaths: Array<{ path: string; prev: unknown; next: unknown }> = [];\n collectChangedPaths(\"\", _state, nextState, changedPaths);\n if (changedPaths.length === 0) return;\n const previousState = _state;\n _state = nextState;\n for (const entry of changedPaths) {\n _emit(entry.path, entry.prev, entry.next, previousState);\n }\n }\n\n function collectChangedPaths(\n prefix: string,\n prev: unknown,\n next: unknown,\n out: Array<{ path: string; prev: unknown; next: unknown }>,\n ): void {\n if (prev === next) return;\n if (\n prev !== null &&\n next !== null &&\n typeof prev === \"object\" &&\n typeof next === \"object\" &&\n !Array.isArray(prev) &&\n !Array.isArray(next)\n ) {\n const allKeys = new Set([...Object.keys(prev as object), ...Object.keys(next as object)]);\n for (const k of allKeys) {\n const pv = (prev as Record<string, unknown>)[k];\n const nv = (next as Record<string, unknown>)[k];\n const childPath = prefix ? `${prefix}.${k}` : k;\n collectChangedPaths(childPath, pv, nv, out);\n }\n } else if (!shallowEqual(prev, next)) {\n out.push({ path: prefix, prev, next });\n }\n }\n\n function batch(fn: (ctx: BatchContext<State>) => void): void {\n let pendingState = _state;\n const changes: Array<{ path: string; prev: unknown; next: unknown; prevState: Readonly<State> }> = [];\n\n const ctx: BatchContext<State> = {\n set<K extends Path<State>>(path: K, value: PathValue<State, K>): void {\n const segments = parsePath(path);\n const previousValue = getAtPath(pendingState, segments);\n if (shallowEqual(previousValue, value)) return;\n const prevState = pendingState;\n pendingState = deepFreeze(setAtPath(deepClone(pendingState), segments, value));\n changes.push({ path, prev: previousValue, next: value, prevState });\n },\n update<K extends Path<State>>(path: K, fn2: (current: PathValue<State, K>) => PathValue<State, K>): void {\n const segments = parsePath(path);\n const current = getAtPath(pendingState, segments) as PathValue<State, K>;\n const next = fn2(deepClone(current));\n ctx.set(path, next);\n },\n patch(partial: DeepPartial<State>): void {\n const merged = deepFreeze(deepMerge(pendingState as State, partial));\n const changedPaths: Array<{ path: string; prev: unknown; next: unknown }> = [];\n collectChangedPaths(\"\", pendingState, merged, changedPaths);\n if (changedPaths.length === 0) return;\n const prevState = pendingState;\n pendingState = merged;\n for (const entry of changedPaths) {\n changes.push({ path: entry.path, prev: entry.prev, next: entry.next, prevState });\n }\n },\n };\n\n fn(ctx);\n\n if (pendingState === _state) return;\n _state = pendingState;\n\n for (const change of changes) {\n _emit(change.path, change.prev, change.next, change.prevState);\n }\n }\n\n function on<E extends keyof ChangeEventMap<State>>(event: E, handler: (payload: ChangeEventMap<State>[E]) => void): void {\n _emitter.on(event, handler as (payload: ChangeEventMap<State>[E]) => void);\n }\n\n function off<E extends keyof ChangeEventMap<State>>(event: E, handler: (payload: ChangeEventMap<State>[E]) => void): void {\n _emitter.off(event, handler as (payload: ChangeEventMap<State>[E]) => void);\n }\n\n function subscribe<K extends Path<State>>(\n path: K,\n handler: (payload: ChangePayload<State, PathValue<State, K>>) => void,\n ): () => void {\n const eventName = `change:${path}` as `change:${string}`;\n const wrapped = (payload: ChangePayload<State>) => handler(payload as unknown as ChangePayload<State, PathValue<State, K>>);\n _emitter.on(eventName, wrapped);\n return () => _emitter.off(eventName, wrapped);\n }\n\n function serialize(): string {\n return JSON.stringify(_state);\n }\n\n function reset(): void {\n const previousState = _state;\n _state = _initial;\n const changes: Array<{ path: string; prev: unknown; next: unknown }> = [];\n collectChangedPaths(\"\", previousState, _state, changes);\n for (const change of changes) {\n _emit(change.path, change.prev, change.next, previousState);\n }\n }\n\n return { get, select, set, update, patch, batch, on, off, subscribe, serialize, reset };\n}","export type FsmResult<T = void> = {\n success: boolean;\n message?: string;\n value?: T;\n};\n\nexport class Fsm {\n public states = new Map<string, FsmState>();\n public current: FsmState | null = null;\n public currentParams: any[] = [];\n public currentTime: number = 0;\n\n constructor() {}\n\n /** Register new states, returns array of registered ExState */\n register(...types: (string | FsmState)[]): FsmResult<FsmState[]> {\n const generatedStates: FsmState[] = [];\n\n for (const state of types) {\n const newState = typeof state === \"string\" ? new FsmState(state) : state;\n\n if (this.states.has(newState.name)) {\n console.warn(`State \"${newState.name}\" is already registered and will be overwritten.`);\n }\n\n this.states.set(newState.name, newState);\n generatedStates.push(newState);\n }\n\n return { success: true, value: generatedStates };\n }\n\n /** Change current state */\n set(state: string | FsmState, ...params: any[]): FsmResult<FsmState> | Promise<FsmResult<FsmState>> {\n const name = typeof state === \"string\" ? state : state.name;\n const next = this.states.get(name);\n\n if (!next) return { success: false, message: `State \"${name}\" not found` };\n\n const finalize = () => {\n this.current = next;\n this.currentParams = params;\n this.currentTime = Date.now();\n return { success: true, value: next };\n };\n\n // No current state, only enter next\n if (!this.current) {\n const entering = next.enter(this.current, ...params);\n return entering instanceof Promise ? entering.then(finalize) : finalize();\n }\n\n // Handle current exit\n const leaving = this.current.exit(next, ...params);\n if (leaving instanceof Promise) {\n return leaving.then(() => {\n const entering = next.enter(this.current, ...params);\n return entering instanceof Promise ? entering.then(finalize) : finalize();\n });\n }\n\n const entering = next.enter(this.current, ...params);\n return entering instanceof Promise ? entering.then(finalize) : finalize();\n }\n\n /** Check if a state is registered */\n has(state: string | FsmState): boolean {\n const name = typeof state === \"string\" ? state : state.name;\n return this.states.has(name);\n }\n\n /** Get current state */\n get(): FsmResult<FsmState> {\n if (!this.current) return { success: false, message: \"No state set\" };\n return { success: true, value: this.current };\n }\n\n /** Reset FSM */\n reset(): FsmResult<void> {\n this.current = null;\n this.currentParams = [];\n this.currentTime = 0;\n return { success: true };\n }\n\n /** Update current state */\n update(): FsmResult<void> | Promise<FsmResult<void>> {\n if (!this.current) return { success: false, message: \"No current state\" };\n\n const result = this.current.update(...this.currentParams);\n return result instanceof Promise ? result.then(() => ({ success: true })) : { success: true };\n }\n}\n\nexport class FsmState {\n constructor(public name: string) {}\n\n enter(_previous: FsmState | null, ..._params: any): void | Promise<void> {}\n exit(_next: FsmState | null, ..._params: any): void | Promise<void> {}\n update(..._params: any): void | Promise<void> {}\n}","import { Directive, TemplateRef, input, inject } from '@angular/core';\n\n/**\n * A directive that marks a template (or element) with a name so it can be\n * queried and rendered manually.\n *\n * This is useful in two main scenarios:\n * - Defining multiple named templates inside a component and choosing which one to render at runtime.\n * - Accepting named templates from a parent component via content projection.\n *\n * ## Supported Syntaxes\n *\n * ```html\n * <!-- On ng-template (recommended for pure templates) -->\n * <ng-template templateSelection=\"header\">...</ng-template>\n * <ng-template templateSelection=\"footer\">...</ng-template>\n *\n * <!-- Using structural directive syntax (also supported) -->\n * <div *templateSelection=\"'sidebar'\">Sidebar content</div>\n * ```\n *\n * ## Consumption Example\n *\n * You can query the templates using either `viewChildren()` or `contentChildren()`,\n * depending on your use case:\n *\n * ```ts\n * @Component({...})\n * export class MyComponent {\n * // Use viewChildren() if the templates are defined inside this component's own template\n * internalTemplates = viewChildren(TemplateSelection);\n *\n * // Use contentChildren() if you want to accept named templates from the parent via content projection\n * projectedTemplates = contentChildren(TemplateSelection);\n *\n * getTemplate(name: string): TemplateRef<any> | undefined {\n * return this.internalTemplates()\n * .find(t => t.name() === name)\n * ?.templateRef;\n * }\n * }\n * ```\n *\n * Then in the template:\n * ```html\n * <ng-container *ngTemplateOutlet=\"getTemplate('header')\"></ng-container>\n * ```\n */\n@Directive({\n selector: '[templateSelection]',\n standalone: true,\n})\nexport class TemplateSelection {\n /**\n * The name used to identify this template.\n * Can be bound as `templateSelection=\"myName\"` or `[templateSelection]=\"someVariable\"`.\n */\n name = input.required<string>({ alias: 'templateSelection' });\n\n /**\n * The underlying TemplateRef that can be used with *ngTemplateOutlet or ViewContainerRef.\n */\n readonly templateRef = inject(TemplateRef);\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tmjeee/w-lib",
3
- "version": "0.0.1",
3
+ "version": "0.1.1",
4
4
  "description": "A minimal, modern TypeScript library skeleton ready for publishing to npmjs.com. Includes helloWorld sample.",
5
5
  "keywords": [
6
6
  "typescript",
@@ -52,8 +52,21 @@
52
52
  "publish:npm": "npm run build && npm run test && npm publish --access public",
53
53
  "publish:npm:dry": "npm run build && npm run test && npm publish --access public --dry-run"
54
54
  },
55
+ "dependencies": {
56
+ "@angular/animations": "^21.2.13",
57
+ "@angular/cdk": "^21.2.11",
58
+ "@angular/common": "^21.2.0",
59
+ "@angular/compiler": "^21.2.0",
60
+ "@angular/core": "^21.2.0",
61
+ "@angular/forms": "^21.2.0",
62
+ "@angular/material": "^21.2.11",
63
+ "@angular/platform-browser": "^21.2.0",
64
+ "@angular/router": "^21.2.0",
65
+ "rxjs": "^7.8.2"
66
+ },
55
67
  "devDependencies": {
56
68
  "@types/node": "^22.15.0",
69
+ "jsdom": "^29.1.1",
57
70
  "tsdown": "^0.20.3",
58
71
  "typescript": "^5.9.3",
59
72
  "vitest": "^4.0.8"