@dharmax/state-router 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/ReadMe.md CHANGED
@@ -6,37 +6,48 @@ What is a functional router? it's a router that captures a url changes and
6
6
  per specific pattern of url, it simply triggers a handler. It supports hash notation
7
7
  as well as normal url patterns. It also supports contexts (url parameters).
8
8
 
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:
9
+ Together with the **state manager** included here - which will be the main object you'd need
10
+ to work with, you get a very friendly and strong semantic application state management:
11
11
  each state can be given a logical name, route and an associated page/component.
12
12
 
13
+ # Features
14
+ * Very easy to use
15
+ * Purely Functional - no need for anything by simple JS
16
+ * Only one tiny dependency
17
+ * Easily allow any authorization logic to be used
18
+ * Tiny footprint
19
+ * Doesn't monopolise the URL
20
+ * Supports state contexts
13
21
 
14
22
  # Example
15
23
 
16
24
  ## state definitions
17
25
 
18
26
  ```javascript
27
+
28
+ // this is how you define the states: logical name, component name, route (as string or regex)
19
29
  stateManager.addState('main', 'main-page', /main$/);
20
30
  stateManager.addState('login', 'login-box', 'login')
21
31
  stateManager.addState('signup', 'signup-box', /signup/)
22
32
  stateManager.addState('my-profile') // will assume the page name and the route are the same...
23
- stateManager.addState('inbox')
33
+ stateManager.addState('inbox') // automatically assume the logical name, component and route are the same
24
34
  stateManager.addState('about')
25
- stateManager.addState('discussion', 'discussion-page', 'discussion/%')
26
- ```
27
-
28
- ## usage in the gui
29
-
30
- In your main component, you write something like that:
31
-
32
- ```javascript
33
-
34
- stateManager.onChange( event => {
35
- // this specific example works with RiotJs, but you get the drift
36
- this.update({
37
- currentPage: event.data.pageName
35
+ stateManager.addState('discussion', 'discussion-page', 'discussion/%') // route with parameter (the % is added to the state's context )
36
+
37
+ // this is an example of how you handle the state change (navigation events)
38
+ stateManager.onChange( event => {
39
+ // this specific example works with RiotJs, but you get the drift
40
+ this.update({
41
+ currentPage: event.data.pageName
42
+ })
38
43
  })
39
- })
40
-
41
44
 
45
+ // this is how you can add hooks by which you can block navigation based
46
+ // on any logic you want.
47
+ stateManager.registerChangeAuthority( async targetState => {
48
+ if ( ! await PermissionManager.isUserAllowedToSeePage(targetState.pageName))
49
+ return false
50
+ return openConfirmDialog('Confirm leaving this page')
51
+ })
52
+
42
53
  ```
package/dist/router.d.ts CHANGED
@@ -3,7 +3,7 @@ declare class Route {
3
3
  re: RegExp;
4
4
  handler: Function;
5
5
  }
6
- export declare type RoutingMode = 'history' | 'hash';
6
+ export type RoutingMode = 'history' | 'hash';
7
7
  export declare const router: {
8
8
  mode: RoutingMode;
9
9
  routes: Route[];
@@ -1,20 +1,23 @@
1
1
  import { RoutingMode } from "./router";
2
2
  import { IPubSubHandle, PubSubEvent } from "@dharmax/pubsub";
3
- export declare type ApplicationStateName = string;
4
- export declare type ApplicationState = {
3
+ export type ApplicationStateName = string;
4
+ export type ApplicationState = {
5
5
  name: ApplicationStateName;
6
6
  pageName: string;
7
7
  route: RegExp;
8
8
  mode?: string | string[];
9
9
  };
10
+ export type ChangeAuthority = (state: ApplicationState) => Promise<boolean>;
10
11
  export declare class StateManager {
11
12
  private allStates;
12
13
  private appState;
13
14
  private previousState;
14
15
  private stateContext;
15
16
  static dispatcher: import("@dharmax/pubsub").PubSub;
17
+ private changeAuthorities;
16
18
  constructor(mode?: RoutingMode);
17
19
  onChange(handler: (event: PubSubEvent, data: any) => void): IPubSubHandle;
20
+ registerChangeAuthority(authorityCallback: (state: ApplicationState) => Promise<boolean>): void;
18
21
  getState(): ApplicationState;
19
22
  get previous(): ApplicationState;
20
23
  get context(): ApplicationState;
@@ -30,7 +33,7 @@ export declare class StateManager {
30
33
  * @param stateName state
31
34
  * @param context extra context (e.g. sub-state)
32
35
  */
33
- setState(stateName: ApplicationStateName, context?: any): boolean;
36
+ setState(stateName: ApplicationStateName, context?: any): Promise<boolean>;
34
37
  /**
35
38
  * Define an application state
36
39
  * @param name
@@ -3,12 +3,21 @@ import dispatcher from "@dharmax/pubsub";
3
3
  export class StateManager {
4
4
  constructor(mode = 'hash') {
5
5
  this.allStates = {};
6
+ this.changeAuthorities = [];
6
7
  router.mode = mode;
7
8
  router.listen();
8
9
  }
9
10
  onChange(handler) {
10
11
  return StateManager.dispatcher.on('state:changed', handler);
11
12
  }
13
+ /*
14
+ Add a hook which enable conditional approval of state change. It can be more than one; when a state
15
+ change is requested, all the registered authorities must return true (asynchronously) otherwise the change
16
+ requested doesn't happen.
17
+ **/
18
+ registerChangeAuthority(authorityCallback) {
19
+ this.changeAuthorities.push(authorityCallback);
20
+ }
12
21
  getState() {
13
22
  return this.appState || {};
14
23
  }
@@ -31,7 +40,7 @@ export class StateManager {
31
40
  /** attempts to restore state from current url */
32
41
  restoreState(defaultState) {
33
42
  let dest = window.location.hash;
34
- if (dest == '#login' || dest == '')
43
+ if (dest === '#login' || dest === '')
35
44
  dest = '#' + defaultState;
36
45
  router.navigate(dest);
37
46
  }
@@ -40,12 +49,17 @@ export class StateManager {
40
49
  * @param stateName state
41
50
  * @param context extra context (e.g. sub-state)
42
51
  */
43
- setState(stateName, context) {
52
+ async setState(stateName, context) {
44
53
  const newState = this.allStates[stateName];
45
54
  if (!newState) {
46
55
  alert(`Undefined app state ${stateName}`);
47
56
  return false;
48
57
  }
58
+ // check if the state change was declined by any change authority and if so - don't do it and return false
59
+ const changeConfirmations = await Promise.all(this.changeAuthorities.map(a => a(newState)));
60
+ if (changeConfirmations.find(c => c === false))
61
+ return false;
62
+ // perform the change
49
63
  this.previousState = this.appState;
50
64
  this.stateContext = context;
51
65
  this.appState = newState;
@@ -75,8 +89,8 @@ export class StateManager {
75
89
  }
76
90
  registerStateByState(state) {
77
91
  this.allStates[state.name] = state;
78
- router.add(state.route, (context) => {
79
- if (this.setState(state.name, context)) {
92
+ router.add(state.route, async (context) => {
93
+ if (await this.setState(state.name, context)) {
80
94
  // @ts-ignore
81
95
  if (window.ga) {
82
96
  // @ts-ignore
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@dharmax/state-router",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "A cute and tight router and application state controller",
5
- "main": "dist/router.js",
5
+ "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "scripts": {
8
8
  "test": "echo \"Error: no test specified\" && exit 1",
@@ -25,7 +25,7 @@
25
25
  },
26
26
  "homepage": "https://github.com/dharmax/state-routerr",
27
27
  "devDependencies": {
28
- "typescript": "^4.4.3"
28
+ "typescript": "^4.9.3"
29
29
  },
30
30
  "dependencies": {
31
31
  "@dharmax/pubsub": "^1.1.0"
@@ -11,6 +11,7 @@ export type ApplicationState = {
11
11
  mode?: string | string[]
12
12
  }
13
13
 
14
+ export type ChangeAuthority = (state: ApplicationState) => Promise<boolean>
14
15
 
15
16
  export class StateManager {
16
17
  private allStates: { [name: string]: ApplicationState } = {}
@@ -19,6 +20,7 @@ export class StateManager {
19
20
  private stateContext: ApplicationState
20
21
 
21
22
  public static dispatcher = dispatcher
23
+ private changeAuthorities: ChangeAuthority[] = [];
22
24
 
23
25
  constructor(mode: RoutingMode = 'hash') {
24
26
 
@@ -27,11 +29,20 @@ export class StateManager {
27
29
 
28
30
  }
29
31
 
30
- onChange(handler: (event: PubSubEvent, data: any) => void):IPubSubHandle {
32
+ onChange(handler: (event: PubSubEvent, data: any) => void): IPubSubHandle {
31
33
 
32
34
  return StateManager.dispatcher.on('state:changed', handler)
33
35
  }
34
36
 
37
+ /*
38
+ Add a hook which enable conditional approval of state change. It can be more than one; when a state
39
+ change is requested, all the registered authorities must return true (asynchronously) otherwise the change
40
+ requested doesn't happen.
41
+ **/
42
+ registerChangeAuthority(authorityCallback: (targetState: ApplicationState) => Promise<boolean>) {
43
+ this.changeAuthorities.push(authorityCallback)
44
+ }
45
+
35
46
  getState(): ApplicationState {
36
47
  return this.appState || <ApplicationState>{}
37
48
  }
@@ -55,7 +66,7 @@ export class StateManager {
55
66
  this.setState(state)
56
67
  }
57
68
 
58
- /** attempts to restore state from current url */
69
+ /** attempts to restore state from current url. Currently, works only in hash mode */
59
70
  restoreState(defaultState: ApplicationStateName) {
60
71
  let dest = window.location.hash
61
72
  if (dest == '#login' || dest == '')
@@ -68,7 +79,7 @@ export class StateManager {
68
79
  * @param stateName state
69
80
  * @param context extra context (e.g. sub-state)
70
81
  */
71
- setState(stateName: ApplicationStateName, context?: any): boolean {
82
+ async setState(stateName: ApplicationStateName, context?: any): Promise<boolean> {
72
83
 
73
84
  const newState = this.allStates[stateName];
74
85
  if (!newState) {
@@ -76,6 +87,12 @@ export class StateManager {
76
87
  return false
77
88
  }
78
89
 
90
+ // check if the state change was declined by any change authority and if so - don't do it and return false
91
+ const changeConfirmations = await Promise.all(this.changeAuthorities.map( authority => authority(newState) ))
92
+ if (changeConfirmations.find( c => c === false))
93
+ return false
94
+
95
+ // perform the change
79
96
  this.previousState = this.appState
80
97
  this.stateContext = context
81
98
  this.appState = newState
@@ -108,15 +125,14 @@ export class StateManager {
108
125
 
109
126
  registerStateByState(state: ApplicationState) {
110
127
  this.allStates[state.name] = state
111
- router.add(state.route, (context: any) => {
112
- if (this.setState(state.name, context)) {
128
+ router.add(state.route, async (context: any) => {
129
+ if (await this.setState(state.name, context)) {
113
130
 
114
131
  // @ts-ignore
115
132
  if (window.ga) {
116
133
  // @ts-ignore
117
134
  window.ga('send', 'pageview', `/${state.name}/${context || ''}`);
118
135
  }
119
-
120
136
  }
121
137
  })
122
138
  }