@nano-app/frontend 0.4.0 → 0.5.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
|
@@ -70,6 +70,14 @@ export default counterController.bindView(({ stores: { app }, increment }) => (
|
|
|
70
70
|
- The view re-renders only when a store path it read changes.
|
|
71
71
|
- A method named `props` or `stores` is hidden by the context of the same name.
|
|
72
72
|
|
|
73
|
+
`extend` returns a new controller with your methods merged over its own: a method with the same name replaces the original, the others stay, and the original controller does not change. Its methods receive the same context (`props`, `stores`) as the controller's.
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
export default counterController.extend(({ stores: { app } }) => ({
|
|
77
|
+
reset: () => app.set('/iCount', 0),
|
|
78
|
+
}));
|
|
79
|
+
```
|
|
80
|
+
|
|
73
81
|
## Backend
|
|
74
82
|
|
|
75
83
|
A store of type `api` connects the app to a backend. The browser never talks to the backend: it calls `/api/<store>/...` on the Next server, and the proxy sends the request there.
|
|
@@ -91,7 +99,7 @@ The backend URL is read at runtime from `API_<STORE>_URL` (`backend` → `API_BA
|
|
|
91
99
|
|
|
92
100
|
### Reading and writing
|
|
93
101
|
|
|
94
|
-
An `api` store has
|
|
102
|
+
An `api` store reads and writes its backend, and keeps what it read. It also has its own state like any store (`get`, `set`, and `data` in its file), apart from what it read: UI state about that backend (a page, an order) can live next to it.
|
|
95
103
|
|
|
96
104
|
```ts
|
|
97
105
|
// src/controllers/users.controller.ts
|
|
@@ -6,16 +6,22 @@ import { enableStaticRendering, observer } from "mobx-react-lite";
|
|
|
6
6
|
enableStaticRendering(IS_SERVER);
|
|
7
7
|
/** Creates a controller: methods on the stores and the props, for the views bound to it */
|
|
8
8
|
function createController(fnMethods) {
|
|
9
|
-
return {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
return {
|
|
10
|
+
bindView: (fnView) => observer((oProps) => {
|
|
11
|
+
const oContext = {
|
|
12
|
+
props: oProps,
|
|
13
|
+
stores: useStores()
|
|
14
|
+
};
|
|
15
|
+
return fnView({
|
|
16
|
+
...fnMethods(oContext),
|
|
17
|
+
...oContext
|
|
18
|
+
});
|
|
19
|
+
}),
|
|
20
|
+
extend: (fnExtension) => createController((oContext) => ({
|
|
15
21
|
...fnMethods(oContext),
|
|
16
|
-
...oContext
|
|
17
|
-
})
|
|
18
|
-
}
|
|
22
|
+
...fnExtension(oContext)
|
|
23
|
+
}))
|
|
24
|
+
};
|
|
19
25
|
}
|
|
20
26
|
//#endregion
|
|
21
27
|
export { createController as default };
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import ApiStore from "../stores/ApiStore.js";
|
|
2
1
|
import Store from "../stores/Store.js";
|
|
2
|
+
import ApiStore from "../stores/ApiStore.js";
|
|
3
3
|
//#region src/creators/createStores.ts
|
|
4
4
|
/** Stores of one React tree, from the ones `withNanoApp` inlined at build time */
|
|
5
5
|
function createStores() {
|
|
6
6
|
const oFiles = JSON.parse(process.env.NANO_APP_STORES ?? "{}");
|
|
7
7
|
return {
|
|
8
8
|
...Object.fromEntries(Object.entries(oFiles).map(([sName, oFile]) => {
|
|
9
|
-
return [sName, oFile.type === "api" ? new ApiStore(sName) : new Store(oFile.data)];
|
|
9
|
+
return [sName, oFile.type === "api" ? new ApiStore(sName, oFile.data) : new Store(oFile.data)];
|
|
10
10
|
})),
|
|
11
11
|
["app"]: new Store(oFiles["app"]?.data)
|
|
12
12
|
};
|
package/build/stores/ApiStore.js
CHANGED
|
@@ -1,37 +1,42 @@
|
|
|
1
1
|
import { IS_SERVER } from "../core/const.js";
|
|
2
|
+
import Store from "./Store.js";
|
|
2
3
|
import { observable, runInAction } from "mobx";
|
|
3
4
|
//#region src/stores/ApiStore.ts
|
|
4
|
-
/**
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Backend data of an `api` store, through its proxy (`/api/<name>/...`), by path and query.
|
|
7
|
+
* Its own state (`get`/`set`) is the one of a `Store`; what it read stays apart.
|
|
8
|
+
*/
|
|
9
|
+
var ApiStore = class ApiStore extends Store {
|
|
6
10
|
sName;
|
|
7
|
-
|
|
11
|
+
oCache = observable({
|
|
8
12
|
iBusy: 0,
|
|
9
13
|
oResults: {},
|
|
10
14
|
oVersions: {}
|
|
11
15
|
});
|
|
12
16
|
oRead = {};
|
|
13
|
-
/** Store of the backend `API_<NAME>_URL
|
|
14
|
-
constructor(sName) {
|
|
17
|
+
/** Store of the backend `API_<NAME>_URL`, with the initial data of its file */
|
|
18
|
+
constructor(sName, oData) {
|
|
19
|
+
super(oData);
|
|
15
20
|
this.sName = sName;
|
|
16
21
|
}
|
|
17
22
|
/** Requests in progress */
|
|
18
23
|
get busy() {
|
|
19
|
-
return this.
|
|
24
|
+
return this.oCache.iBusy;
|
|
20
25
|
}
|
|
21
26
|
/** Result at a path: from local, asked to the backend when never read or its entity changed */
|
|
22
27
|
read(sPath, oQuery) {
|
|
23
28
|
if (IS_SERVER) return void 0;
|
|
24
29
|
const sKey = oQuery ? `${sPath}?${JSON.stringify(oQuery)}` : sPath;
|
|
25
|
-
const iVersion = this.
|
|
30
|
+
const iVersion = this.oCache.oVersions[ApiStore.entity(sPath)] ?? 0;
|
|
26
31
|
if (this.oRead[sKey] !== iVersion) {
|
|
27
32
|
this.oRead[sKey] = iVersion;
|
|
28
33
|
const oInit = oQuery ? {
|
|
29
34
|
method: "QUERY",
|
|
30
35
|
body: JSON.stringify(oQuery)
|
|
31
36
|
} : { method: "GET" };
|
|
32
|
-
queueMicrotask(() => this.request(sPath, oInit).then((oResponse) => runInAction(() => this.
|
|
37
|
+
queueMicrotask(() => this.request(sPath, oInit).then((oResponse) => runInAction(() => this.oCache.oResults[sKey] = oResponse.result)));
|
|
33
38
|
}
|
|
34
|
-
return this.
|
|
39
|
+
return this.oCache.oResults[sKey];
|
|
35
40
|
}
|
|
36
41
|
/** `POST <path>` */
|
|
37
42
|
create(sPath, oBody) {
|
|
@@ -52,19 +57,19 @@ var ApiStore = class ApiStore {
|
|
|
52
57
|
body: JSON.stringify(oBody)
|
|
53
58
|
}).finally(() => runInAction(() => {
|
|
54
59
|
const sEntity = ApiStore.entity(sPath);
|
|
55
|
-
this.
|
|
60
|
+
this.oCache.oVersions[sEntity] = (this.oCache.oVersions[sEntity] ?? 0) + 1;
|
|
56
61
|
}));
|
|
57
62
|
}
|
|
58
63
|
/** Sends a request to the proxy and returns its JSON answer, counting it as busy */
|
|
59
64
|
async request(sPath, oInit) {
|
|
60
|
-
runInAction(() => this.
|
|
65
|
+
runInAction(() => this.oCache.iBusy++);
|
|
61
66
|
try {
|
|
62
67
|
return await (await fetch(`/api/${this.sName}${sPath}`, {
|
|
63
68
|
...oInit,
|
|
64
69
|
headers: { "content-type": "application/json" }
|
|
65
70
|
})).json();
|
|
66
71
|
} finally {
|
|
67
|
-
runInAction(() => this.
|
|
72
|
+
runInAction(() => this.oCache.iBusy--);
|
|
68
73
|
}
|
|
69
74
|
}
|
|
70
75
|
/** Entity of a path: its first segment (`/users/7` → `/users`) */
|
|
@@ -15,5 +15,7 @@ export type View<M extends NanoObject> = (params: Omit<M, keyof ComponentContext
|
|
|
15
15
|
export type Controller<M extends NanoObject> = {
|
|
16
16
|
/** Creates a component that renders the view with the controller's methods */
|
|
17
17
|
bindView(view: View<M>): (props: ComponentProps) => ReactNode;
|
|
18
|
+
/** Creates a controller with these methods merged over its own: a method with the same name wins */
|
|
19
|
+
extend<E extends NanoObject>(methods: (context: ComponentContext) => E): Controller<Omit<M, keyof E> & E>;
|
|
18
20
|
};
|
|
19
21
|
//#endregion
|