@dharmax/state-router 3.1.2 → 3.2.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/ReadMe.md CHANGED
@@ -1,57 +1,188 @@
1
1
 
2
- # General
3
- This package contains a functional router and a web-application state manager.
2
+ ## Overview
4
3
 
5
- What is a functional router? it's a router that captures a url changes and
6
- per specific pattern of url, it simply triggers a handler. It supports hash notation
7
- as well as normal url patterns. It also supports contexts (url parameters).
4
+ This package contains a tiny, functional router and a web‑application state manager.
8
5
 
9
- Together with the state manager - which will be the main object you'd need
10
- to work with, you get a very simple semantic application state management:
11
- each state can be given a logical name, route and an associated page/component.
6
+ The router captures URL changes and triggers a handler for the first matching route pattern. It supports both hash (`#/path`) and history (`/path`) modes, and passes route parameters and query data to your handler.
12
7
 
13
- The router by default ignores file extensions '.json', '.css', '.js', '.png', '.jpg', '.svg', '.webp','md'
14
- and you can access the router's staticFilters member to replace or add other rules
15
- for static serving support.
8
+ The state manager provides a minimal semantic state layer on top of the router: define named states, their route, and optional mode(s); listen for changes; and gate transitions with async guards.
16
9
 
17
- you can use the hash notation or not (set the router's mode to 'history' or 'hash')
10
+ - Static files: the router ignores common static file extensions (e.g. `.css`, `.js`, `.png`, `.svg`, `.webp`, `.json`, `.md`, `.txt`, `.ejs`, `.jsm`). You can customize `router.staticFilters` to adjust.
11
+ - Modes: use `router.listen('hash' | 'history')`. For static file serving (file:// or a simple static server), prefer `hash`.
18
12
 
13
+ ## Installation
19
14
 
20
- # Example
15
+ Install as usual and build the TypeScript sources:
21
16
 
22
- ## state definitions
17
+ ```
18
+ npm install
19
+ npm run build
20
+ ```
21
+
22
+ ## Quick Start
23
+
24
+ ```ts
25
+ import { router, StateManager } from '@dharmax/state-router'
26
+
27
+ // Router: match params and use query context
28
+ router
29
+ .add(/^user\/(\d+)$/, function (id: string) {
30
+ // `this` holds query params from the current URL
31
+ // @ts-ignore
32
+ console.log('user', id, 'q=', this.queryParams?.q)
33
+ })
34
+ .listen('hash')
35
+
36
+ // State Manager: define states and react
37
+ const sm = new StateManager('hash')
38
+ sm.addState('home', 'home', /^home$/)
39
+ sm.addState('post', 'post', /^post\/(\w+)$/)
23
40
 
24
- ```javascript
25
- stateManager.addState('main', 'main-page', /main$/);
26
- stateManager.addState('login', 'login-box', 'login')
27
- stateManager.addState('signup', 'signup-box', /signup/)
28
- stateManager.addState('my-profile') // will assume the page name and the route are the same...
29
- stateManager.addState('inbox')
30
- stateManager.addState('about')
31
- stateManager.addState('discussion', 'discussion-page', 'discussion/%')
32
- stateManager.addState('article', 'article-page', 'article/%','article-mode')
33
- stateManager.addState('blog', 'article-page', 'blog/%','blog-mode')
41
+ sm.onChange((event, state) => {
42
+ console.log('state changed to', state.name, 'context=', sm.context)
43
+ })
44
+
45
+ // Navigate
46
+ router.navigate('home')
47
+ router.navigate('post/hello')
34
48
  ```
35
49
 
36
- ## usage in the gui
50
+ ## Router API
51
+
52
+ - `router.add(pattern: RegExp | RouteHandler, handler?: RouteHandler)`
53
+ - If `pattern` is a RegExp, captured groups are passed as handler arguments.
54
+ - If `pattern` is omitted (i.e., you pass only a function), it becomes a catch‑all route.
55
+ - The handler’s `this` contains `queryParams` built from `window.location.search`.
56
+ - String patterns with named params are supported: `'/users/:id'` produces `this.params = { id: '...' }` and still passes the captured values as handler args.
57
+
58
+ - `router.listen(mode?: 'hash' | 'history')`
59
+ - In `history` mode, internal `<a href="/...">` clicks and Enter on focused links are intercepted.
60
+ - Interception is hardened: uses `closest('a')`; ignores modified clicks (meta/ctrl/shift), non‑left clicks, `target=_blank`, `download`, `rel=noreferrer`, and external origins or different hostnames; and skips static‑looking URLs (by extension).
61
+
62
+ - `router.navigate(path: string, opts?: { replace?: boolean })`
63
+ - Navigates according to the active mode and triggers routing.
64
+ - If `opts.replace` is true (history mode), uses `history.replaceState` instead of `pushState`.
65
+
66
+ - `router.replace(path: string)`
67
+ - Shorthand for `router.navigate(path, { replace: true })`.
68
+
69
+ - `router.resetRoot(root: string)`
70
+ - Set a base root for history URL calculation.
71
+
72
+ - `router.unlisten()`
73
+ - Removes all listeners previously attached by `listen()` (click, keydown, popstate/hashchange). Useful for cleanup and tests.
74
+
75
+ - `router.onNotFound(handler)`
76
+ - Registers a fallback called when no routes match. Returns `true` from `handleChange()` after invoking the hook.
77
+
78
+ - `router.getQueryParams(search?: string)`
79
+ - Returns a parsed query map from the current URL (or from a provided search string). Useful if you’d rather not use handler `this`.
80
+
81
+ - `router.setDecodeParams(boolean)`
82
+ - Optionally `decodeURIComponent` route parameters before passing them to handlers and `this.params`.
83
+
84
+ Notes: the router lazily accesses `window`/`document` to be SSR‑safe; outside a browser environment, listeners are not attached and navigation no‑ops.
85
+
86
+ - `createRouter()`
87
+ - Factory that returns a fresh Router instance. Useful for testing or isolating multiple routers.
88
+
89
+ ## State Manager API
90
+
91
+ - `new StateManager(mode?: 'hash' | 'history', autostart = true, routerInstance = router)`
92
+ - When `autostart` is true, calls `router.listen(mode)` automatically.
93
+ - You can pass a custom router instance (e.g., from `createRouter()`) for isolation.
94
+ - Call `sm.stop()` to unlisten the router.
95
+
96
+ - `addState(name, pageName?, route?: RegExp | string, mode?: string | string[])`
97
+ - If `route` is a string and contains `%`, each `%` is expanded to a non‑mandatory capture `?(.*)` for “the rest of the path”. For example, `'docs%'` becomes `^docs?(.*)$` and the first capture is provided as the state context (e.g., `'/guide'`).
98
+ - If `route` is a RegExp, the first capturing group is passed as the state context.
37
99
 
38
- In your main component, you write something like that:
100
+ - `setState(name, context?)`
101
+ - Programmatically set the state and optional context (e.g., a sub‑state or id).
39
102
 
40
- ```javascript
103
+ - `getState()` / `previous` / `context`
104
+ - Access current, previous state, and the last context value.
105
+ - Context can be a string, an array (for multi‑capture regex), or an object (for named params).
41
106
 
42
- stateManager.onChange( event => {
43
- // this specific example works with RiotJs, but you get the drift
44
- this.update({
45
- currentPage: event.data.pageName
46
- })
107
+ - `onChange(handler)`
108
+ - Subscribes to `state:changed` events via `@dharmax/pubsub`.
109
+
110
+ - `onBeforeChange(handler)` / `onAfterChange(handler)`
111
+ - Optional hooks around transitions. `onBeforeChange` can veto by returning `false` (sync or async). `onAfterChange` runs after a successful transition.
112
+
113
+ - `onNotFound(handler)`
114
+ - Subscribe to router‐level notFound events via the state manager for convenience.
115
+
116
+ - `registerChangeAuthority(authority: (target) => Promise<boolean>)`
117
+ - All registered authorities must return `true` to allow a transition.
118
+
119
+ - `restoreState(defaultState)`
120
+ - Attempts to restore from current URL; otherwise navigates to the default state (hash mode).
121
+
122
+ - `createStateManager(mode?: 'hash' | 'history', autostart = true, routerInstance = router)`
123
+ - Factory returning a new StateManager; pass a custom router if desired.
124
+
125
+ ## Data, Context, and Parameter Passing
126
+
127
+ - Route parameters: each capturing group in your route RegExp is passed to the route handler as an argument in order. For `^user\/(\d+)$`, the handler receives the user id string.
128
+ - Query params: inside a route handler, `this.queryParams` exposes an object of the URL’s query parameters (e.g., `{ q: 'hello' }`).
129
+ - State context: when a route defined via `addState` matches, the first capture group is forwarded to the StateManager as the state “context”. Access it via `stateManager.context` after the transition.
130
+
131
+ ## Examples
132
+
133
+ ```ts
134
+ // 1) Params + query
135
+ router.add(/^user\/(\d+)$/, function (id) {
136
+ // @ts-ignore
137
+ const { q } = this.queryParams
138
+ console.log('id=', id, 'q=', q)
47
139
  })
48
140
 
141
+ // 2) State context from route
142
+ sm.addState('docs', 'docs', 'docs%') // captures the suffix as context, e.g. '/guide'
49
143
 
144
+ // 3) Async guard
145
+ sm.registerChangeAuthority(async (target) => {
146
+ return target.name !== 'admin-only'
147
+ })
50
148
  ```
51
149
 
52
- ## More
53
- * pageChangeHandler - it's a global optional page change listener that receives ('send', 'pageview', `/${state.name}/${context || ''}`);
54
- * google analytics (ga) is automatically used if it was found
55
- * query parameters are passed to the state context under queryParams object
56
- * on page change, an event state:changed is fired with the new state (that include the context)
57
- * in addState, the last (optional) parameter can contain either a string an array of strings and it can be used for in-state special logic, or sub-states within the same parent-component, for example
150
+ ## Development & Testing
151
+
152
+ - Build: `npm run build` compiles TypeScript into `dist/`.
153
+ - Manual demo: serve `test/` (e.g., `npx http-server test`) after build. Use `hash` mode for static servers.
154
+ - Automated tests: Vitest + jsdom
155
+ - Run once with coverage: `npm test`
156
+ - Watch mode: `npm run test:watch`
157
+ - Notes:
158
+ - Tests use dynamic imports with `vi.resetModules()` to isolate the singleton router/state manager between cases.
159
+ - Some tests mock `URLSearchParams` to simulate query strings in jsdom without full navigation.
160
+ - Tests use history mode in jsdom via `history.pushState` and `popstate` events; avoid direct `window.location.search = '...'` (jsdom limitation).
161
+
162
+ ## History Mode Server Config
163
+
164
+ When using `history` mode, your server must serve your SPA entry (e.g., `index.html`) for application routes to avoid 404s on refresh or deep links. Static assets should still be served normally.
165
+
166
+ Examples:
167
+
168
+ - Node/Express
169
+ - Serve static first, then a catch‑all returning `index.html`.
170
+ - `app.use(express.static('public'))`
171
+ - `app.get('*', (req, res) => res.sendFile(path.join(__dirname, 'public/index.html')))`
172
+
173
+ - Nginx
174
+ - In your `location /` block: `try_files $uri /index.html;`
175
+
176
+ - Apache
177
+ - Use `FallbackResource /index.html` or an `.htaccess` rewrite.
178
+
179
+ Tip: Keep `router.staticFilters` tuned so links to real files (e.g., `/assets/app.css`) are not intercepted.
180
+
181
+ ## Analytics Hooks
182
+
183
+ If present, the following globals will be invoked on successful state changes:
184
+
185
+ - `window.pageChangeHandler('send', 'pageview', '/<state>/<context>')`
186
+ - `window.ga('send', 'pageview', '/<state>/<context>')`
187
+
188
+ These are optional and ignored if missing.
package/dist/router.d.ts CHANGED
@@ -1,18 +1,28 @@
1
1
  type RouteHandler = (...args: string[]) => void;
2
2
  export type RoutingMode = 'history' | 'hash';
3
- declare class Router {
3
+ export declare class Router {
4
4
  private mode;
5
5
  private routes;
6
6
  private root;
7
+ private rootCompare;
7
8
  private baseLocation;
8
9
  staticFilters: ((url: string) => boolean)[];
10
+ private isListening;
11
+ private bound;
12
+ private notFoundHandler?;
13
+ private decodeParams;
9
14
  constructor();
10
15
  private cleanPathString;
11
16
  private clearQuery;
12
17
  private isStaticFile;
13
18
  resetRoot(root: string): void;
14
19
  getLocation(): string;
15
- add(pattern: RegExp | RouteHandler, handler?: RouteHandler): Router;
20
+ add(pattern: RegExp | string | RouteHandler, handler?: RouteHandler): Router;
21
+ onNotFound(handler: (path: string) => void): Router;
22
+ setDecodeParams(decode: boolean): Router;
23
+ getQueryParams(search?: string): Record<string, string>;
24
+ private isBrowser;
25
+ private compilePattern;
16
26
  /**
17
27
  *
18
28
  * @param location
@@ -20,7 +30,12 @@ declare class Router {
20
30
  */
21
31
  handleChange(location?: string): boolean;
22
32
  listen(mode?: RoutingMode): void;
23
- navigate(path?: string): boolean;
33
+ unlisten(): void;
34
+ navigate(path?: string, opts?: {
35
+ replace?: boolean;
36
+ }): boolean;
37
+ replace(path?: string): boolean;
24
38
  }
25
39
  export declare const router: Router;
40
+ export declare function createRouter(): Router;
26
41
  export {};
package/dist/router.js CHANGED
@@ -1,9 +1,14 @@
1
- class Router {
1
+ export class Router {
2
2
  mode = 'hash';
3
3
  routes = [];
4
4
  root = '/';
5
+ rootCompare = '';
5
6
  baseLocation = null;
6
7
  staticFilters = [];
8
+ isListening = false;
9
+ bound = {};
10
+ notFoundHandler;
11
+ decodeParams = false;
7
12
  constructor() {
8
13
  this.staticFilters.push(url => {
9
14
  const staticFileExtensions = ['.json', '.css', '.js', '.png', '.jpg', '.svg', '.webp', '.md', '.ejs', '.jsm', '.txt'];
@@ -25,13 +30,23 @@ class Router {
25
30
  return (this.staticFilters || []).some(filter => filter(url));
26
31
  }
27
32
  resetRoot(root) {
28
- this.root = '/' + this.cleanPathString(root) + '/';
33
+ const cleaned = this.cleanPathString(root);
34
+ this.root = '/' + cleaned + '/';
35
+ this.rootCompare = cleaned ? cleaned + '/' : '';
29
36
  }
30
37
  getLocation() {
38
+ if (!this.isBrowser())
39
+ return '';
31
40
  if (this.mode === 'history') {
32
- let fragment = this.cleanPathString(decodeURI(window.location.pathname + window.location.search));
41
+ let fragment = decodeURI(window.location.pathname + window.location.search);
33
42
  fragment = this.clearQuery(fragment);
34
- return this.root !== '/' ? fragment.replace(this.root, '') : fragment;
43
+ // strip leading slash for comparison convenience
44
+ fragment = fragment.replace(/^\//, '');
45
+ if (this.root !== '/' && this.rootCompare && fragment.startsWith(this.rootCompare)) {
46
+ fragment = fragment.slice(this.rootCompare.length);
47
+ }
48
+ fragment = this.cleanPathString(fragment);
49
+ return fragment;
35
50
  }
36
51
  else {
37
52
  const match = window.location.href.match(/#(.*)$/);
@@ -39,77 +54,207 @@ class Router {
39
54
  }
40
55
  }
41
56
  add(pattern, handler) {
57
+ let paramNames;
42
58
  if (typeof pattern === 'function') {
43
59
  handler = pattern;
44
60
  pattern = /^.*$/; // Match any path
45
61
  }
46
- this.routes.push({ pattern, handler: handler });
62
+ else if (typeof pattern === 'string') {
63
+ const compiled = this.compilePattern(pattern);
64
+ pattern = compiled.regex;
65
+ paramNames = compiled.paramNames;
66
+ }
67
+ this.routes.push({ pattern: pattern, handler: handler, paramNames });
68
+ return this;
69
+ }
70
+ onNotFound(handler) {
71
+ this.notFoundHandler = handler;
72
+ return this;
73
+ }
74
+ setDecodeParams(decode) {
75
+ this.decodeParams = decode;
47
76
  return this;
48
77
  }
78
+ getQueryParams(search) {
79
+ if (!this.isBrowser() && !search)
80
+ return {};
81
+ const qs = typeof search === 'string' ? search : window.location.search || '';
82
+ const usp = new URLSearchParams(qs);
83
+ const obj = {};
84
+ usp.forEach((v, k) => { obj[k] = v; });
85
+ return obj;
86
+ }
87
+ isBrowser() {
88
+ return typeof window !== 'undefined' && typeof document !== 'undefined';
89
+ }
90
+ compilePattern(pattern) {
91
+ // normalize leading slash to align with cleanPathString behavior
92
+ const normalized = pattern.replace(/^\//, '');
93
+ const paramNames = [];
94
+ // convert /users/:id -> ^users/([^/]+)$
95
+ const reStr = normalized
96
+ .replace(/([.*+?^${}()|[\]\\])/g, '\\$1') // escape regex specials
97
+ .replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_m, p1) => {
98
+ paramNames.push(p1);
99
+ return '([^/]+)';
100
+ });
101
+ return { regex: new RegExp(`^${reStr}$`), paramNames };
102
+ }
49
103
  /**
50
104
  *
51
105
  * @param location
52
106
  * @return true if it was intercepted or false if not handled
53
107
  */
54
108
  handleChange(location) {
55
- const path = location || this.getLocation();
109
+ const path = (location ?? this.getLocation()) || '';
56
110
  if (this.isStaticFile(path))
57
111
  return false; // Bypass routing for static files
58
112
  for (const route of this.routes) {
59
- const match = path.match(route.pattern);
113
+ const match = route.pattern ? path.match(route.pattern) : null;
60
114
  if (match) {
61
115
  match.shift(); // Remove the full match element
62
- const queryParams = Object.fromEntries(new URLSearchParams(window.location.search));
63
- route.handler.apply({ queryParams }, match);
116
+ const queryParams = this.getQueryParams();
117
+ const captures = this.decodeParams ? match.map(v => safeDecode(v)) : match;
118
+ let params;
119
+ if (route.paramNames && route.paramNames.length) {
120
+ params = {};
121
+ route.paramNames.forEach((n, i) => params[n] = captures[i]);
122
+ }
123
+ route.handler.call({ queryParams, params }, ...captures);
64
124
  return true;
65
125
  }
66
126
  }
67
- console.warn(`No routing found for ${path}`);
127
+ if (this.notFoundHandler) {
128
+ this.notFoundHandler(path);
129
+ return true;
130
+ }
131
+ if (path)
132
+ console.warn(`No routing found for ${path}`);
68
133
  return false;
69
134
  }
70
135
  listen(mode = 'hash') {
136
+ if (!this.isBrowser())
137
+ return;
138
+ // avoid duplicate listeners
139
+ if (this.isListening)
140
+ this.unlisten();
71
141
  const self = this;
72
142
  this.mode = mode;
73
- switch (mode) {
74
- case "hash":
75
- window.addEventListener('hashchange', () => handler());
76
- break;
77
- case "history":
78
- window.addEventListener('popstate', event => handler(event.state?.path));
79
- document.addEventListener('click', handleInternalNavigation);
80
- document.addEventListener('keydown', event => {
81
- // @ts-ignore
82
- if (event.key === 'Enter' && event.target.tagName === 'A')
83
- handleInternalNavigation(event);
84
- });
85
- }
86
- function handleInternalNavigation(event) {
87
- const node = event.target;
88
- const href = node.getAttribute('href');
89
- if (href) {
90
- event.preventDefault();
91
- history.pushState({ path: href }, '', href);
92
- handler(href);
93
- }
94
- }
95
- function handler(path) {
96
- path = path || location.href.split('#')[0];
97
- if (self.isStaticFile(path))
143
+ const handler = (path) => {
144
+ const p = path || location.href.split('#')[0];
145
+ if (self.isStaticFile(p))
98
146
  return;
99
147
  const currentLocation = self.getLocation();
100
148
  if (self.baseLocation !== currentLocation) {
101
149
  self.baseLocation = currentLocation;
102
150
  self.handleChange(currentLocation);
103
151
  }
152
+ };
153
+ const handleInternalNavigation = (event) => {
154
+ // modified clicks or non-left clicks
155
+ const me = event;
156
+ if (event.type === 'click') {
157
+ if (me.button !== 0)
158
+ return; // left-click only
159
+ if (me.metaKey || me.ctrlKey || me.shiftKey)
160
+ return;
161
+ }
162
+ const target = event.target;
163
+ if (!target || !('closest' in target))
164
+ return;
165
+ const anchor = target.closest?.('a');
166
+ if (!anchor)
167
+ return;
168
+ if (event.type === 'keydown') {
169
+ const ke = event;
170
+ if (ke.key !== 'Enter')
171
+ return;
172
+ }
173
+ const href = anchor.getAttribute('href') || '';
174
+ if (!href)
175
+ return;
176
+ const url = new URL(href, window.location.href);
177
+ // ignore external origins or different hostnames
178
+ if (url.origin !== window.location.origin)
179
+ return;
180
+ // ignore target=_blank, download, or rel=noreferrer
181
+ const t = anchor.getAttribute('target');
182
+ if (t && t.toLowerCase() === '_blank')
183
+ return;
184
+ if (anchor.hasAttribute('download'))
185
+ return;
186
+ const rel = anchor.getAttribute('rel');
187
+ if (rel && /\bnoreferrer\b/i.test(rel))
188
+ return;
189
+ const pathWithQuery = url.pathname + (url.search || '');
190
+ if (self.isStaticFile(pathWithQuery) || self.isStaticFile(url.pathname))
191
+ return;
192
+ event.preventDefault();
193
+ history.pushState({ path: pathWithQuery }, '', pathWithQuery);
194
+ handler(pathWithQuery);
195
+ };
196
+ switch (mode) {
197
+ case 'hash':
198
+ this.bound.hashchange = () => handler();
199
+ window.addEventListener('hashchange', this.bound.hashchange);
200
+ break;
201
+ case 'history':
202
+ this.bound.popstate = (event) => handler(event.state?.path);
203
+ this.bound.click = handleInternalNavigation;
204
+ this.bound.keydown = (event) => handleInternalNavigation(event);
205
+ window.addEventListener('popstate', this.bound.popstate);
206
+ document.addEventListener('click', this.bound.click);
207
+ document.addEventListener('keydown', this.bound.keydown);
208
+ break;
104
209
  }
210
+ this.isListening = true;
105
211
  handler();
106
212
  }
107
- navigate(path = '') {
108
- if (this.mode === 'history')
109
- history.pushState(null, null, this.root + this.cleanPathString(path));
213
+ unlisten() {
214
+ if (!this.isBrowser() || !this.isListening)
215
+ return;
216
+ switch (this.mode) {
217
+ case 'hash':
218
+ if (this.bound.hashchange)
219
+ window.removeEventListener('hashchange', this.bound.hashchange);
220
+ break;
221
+ case 'history':
222
+ if (this.bound.popstate)
223
+ window.removeEventListener('popstate', this.bound.popstate);
224
+ if (this.bound.click)
225
+ document.removeEventListener('click', this.bound.click);
226
+ if (this.bound.keydown)
227
+ document.removeEventListener('keydown', this.bound.keydown);
228
+ break;
229
+ }
230
+ this.bound = {};
231
+ this.isListening = false;
232
+ }
233
+ navigate(path = '', opts) {
234
+ if (!this.isBrowser())
235
+ return false;
236
+ if (this.mode === 'history') {
237
+ const url = this.root + this.cleanPathString(path);
238
+ if (opts?.replace)
239
+ history.replaceState(null, '', url);
240
+ else
241
+ history.pushState(null, '', url);
242
+ }
110
243
  else
111
244
  window.location.hash = this.cleanPathString(path);
112
245
  return this.handleChange();
113
246
  }
247
+ replace(path = '') {
248
+ return this.navigate(path, { replace: true });
249
+ }
114
250
  }
115
251
  export const router = new Router();
252
+ export function createRouter() { return new Router(); }
253
+ function safeDecode(v) {
254
+ try {
255
+ return decodeURIComponent(v);
256
+ }
257
+ catch {
258
+ return v;
259
+ }
260
+ }
@@ -1,10 +1,10 @@
1
- import { RoutingMode } from "./router";
1
+ import { RoutingMode, Router as RouterType } from "./router";
2
2
  import { IPubSubHandle, PubSubEvent } from "@dharmax/pubsub";
3
3
  export type ApplicationStateName = string;
4
4
  export type ApplicationState = {
5
5
  name: ApplicationStateName;
6
6
  pageName: string;
7
- route: RegExp;
7
+ route: RegExp | string;
8
8
  mode?: string | string[];
9
9
  };
10
10
  export type ChangeAuthority = (state: ApplicationState) => Promise<boolean>;
@@ -16,13 +16,17 @@ export declare class StateManager {
16
16
  private stateContext;
17
17
  static dispatcher: import("@dharmax/pubsub").PubSub;
18
18
  private changeAuthorities;
19
- constructor(mode?: RoutingMode, autostart?: boolean);
19
+ private router;
20
+ private beforeChangeHandlers;
21
+ private afterChangeHandlers;
22
+ constructor(mode?: RoutingMode, autostart?: boolean, routerInstance?: RouterType);
20
23
  start(): void;
24
+ stop(): void;
21
25
  onChange(handler: (event: PubSubEvent, data: any) => void): IPubSubHandle;
22
26
  registerChangeAuthority(authorityCallback: (targetState: ApplicationState) => Promise<boolean>): void;
23
27
  getState(): ApplicationState;
24
28
  get previous(): ApplicationState;
25
- get context(): ApplicationState;
29
+ get context(): any;
26
30
  /**
27
31
  * set current page state
28
32
  * @param state can be either just a state or a state and context (which can be sub-state, or anything else)
@@ -45,4 +49,8 @@ export declare class StateManager {
45
49
  */
46
50
  addState(name: string, pageName?: string, route?: RegExp | string, mode?: string | string[]): void;
47
51
  registerStateByState(state: ApplicationState): void;
52
+ onBeforeChange(handler: (target: ApplicationState, context?: any) => boolean | Promise<boolean>): void;
53
+ onAfterChange(handler: (state: ApplicationState, context?: any, previous?: ApplicationState) => void | Promise<void>): void;
54
+ onNotFound(handler: (path: string) => void): void;
48
55
  }
56
+ export declare function createStateManager(mode?: RoutingMode, autostart?: boolean, routerInstance?: RouterType): StateManager;