@nan0web/ui 1.0.2 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -35
- package/package.json +7 -2
- package/src/App/Core/CoreApp.js +14 -19
- package/src/App/Core/UI.js +5 -5
- package/src/App/User/UserApp.js +4 -19
- package/src/App/User/UserUI.js +4 -4
- package/src/App/User/index.js +0 -6
- package/src/App/index.js +0 -3
- package/src/README.md.js +3 -27
- package/src/StdIn.js +1 -3
- package/src/core/Error/index.js +9 -0
- package/src/core/Form/Form.js +36 -18
- package/src/core/Form/Input.js +16 -7
- package/src/core/Form/Message.js +6 -9
- package/src/core/InputAdapter.js +4 -0
- package/src/core/Message/InputMessage.js +11 -11
- package/src/core/Message/OutputMessage.js +1 -1
- package/src/core/index.js +2 -0
- package/src/index.js +1 -0
- package/types/App/Core/CoreApp.d.ts +9 -9
- package/types/App/Core/UI.d.ts +5 -5
- package/types/App/Core/Widget.d.ts +1 -1
- package/types/App/User/Command/Message.d.ts +2 -3
- package/types/App/User/Command/Options.d.ts +9 -2
- package/types/App/User/Command/index.d.ts +1 -4
- package/types/App/User/UserApp.d.ts +10 -7
- package/types/App/User/UserUI.d.ts +0 -9
- package/types/App/User/index.d.ts +0 -4
- package/types/App/index.d.ts +1 -3
- package/types/Frame/Frame.d.ts +5 -5
- package/types/View/View.d.ts +3 -3
- package/types/core/Error/index.d.ts +6 -0
- package/types/core/Form/Form.d.ts +12 -6
- package/types/core/Form/Input.d.ts +20 -7
- package/types/core/Form/Message.d.ts +10 -4
- package/types/core/InputAdapter.d.ts +2 -0
- package/types/core/Message/InputMessage.d.ts +5 -5
- package/types/core/Message/OutputMessage.d.ts +1 -1
- package/types/core/Stream.d.ts +1 -1
- package/types/core/StreamEntry.d.ts +1 -1
- package/types/core/index.d.ts +1 -0
- package/types/index.d.ts +1 -0
- package/src/App/Command/Options.js +0 -78
- package/src/App/Command/index.js +0 -9
package/README.md
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
# @nan0web/ui
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|---|---|---|---|---|
|
|
5
|
-
|🟢 `96.8%` |🧪 [English 🏴](https://github.com/nan0web/ui/blob/main/README.md)<br />[Українською 🇺🇦](https://github.com/nan0web/ui/blob/main/docs/uk/README.md) |🟡 `81.1%` |✅ d.ts 📜 system.md 🕹️ playground |1.0.1 |
|
|
3
|
+
<!-- %PACKAGE_STATUS% -->
|
|
6
4
|
|
|
7
5
|
A lightweight, agnostic UI framework designed with the **nan0web philosophy**
|
|
8
6
|
— one application logic, many UI implementations.
|
|
@@ -53,10 +51,9 @@ decoupled communication systems between UI components.
|
|
|
53
51
|
How to create input and output messages?
|
|
54
52
|
```js
|
|
55
53
|
import { InputMessage, OutputMessage } from '@nan0web/ui'
|
|
56
|
-
|
|
57
54
|
const input = InputMessage.from({ value: 'Hello User' })
|
|
58
55
|
const output = OutputMessage.from({ content: ['Welcome to @nan0web/ui'] })
|
|
59
|
-
console.info(input.value) // ← Hello User
|
|
56
|
+
console.info(input.value) // ← Message { body: "Hello User", head: {} }
|
|
60
57
|
console.info(output.content[0]) // ← Welcome to @nan0web/ui
|
|
61
58
|
```
|
|
62
59
|
### Forms
|
|
@@ -76,7 +73,6 @@ Field types include:
|
|
|
76
73
|
How to define and validate a UIForm?
|
|
77
74
|
```js
|
|
78
75
|
import { UIForm } from '@nan0web/ui'
|
|
79
|
-
|
|
80
76
|
const form = new UIForm({
|
|
81
77
|
title: "Contact Form",
|
|
82
78
|
fields: [
|
|
@@ -88,11 +84,9 @@ const form = new UIForm({
|
|
|
88
84
|
message: "Hello!"
|
|
89
85
|
}
|
|
90
86
|
})
|
|
91
|
-
|
|
92
87
|
const result = form.validate()
|
|
93
88
|
console.info(result.isValid) // ← false
|
|
94
89
|
console.info(result.errors.email) // ← Invalid email format
|
|
95
|
-
|
|
96
90
|
```
|
|
97
91
|
### Components
|
|
98
92
|
|
|
@@ -104,7 +98,6 @@ Components render data as frame-ready output.
|
|
|
104
98
|
How to render the Welcome component?
|
|
105
99
|
```js
|
|
106
100
|
import { Welcome } from '@nan0web/ui'
|
|
107
|
-
|
|
108
101
|
const frame = Welcome({ user: { name: "Alice" } })
|
|
109
102
|
const firstLine = frame[0].join("")
|
|
110
103
|
console.info(firstLine) // ← Welcome Alice!
|
|
@@ -122,7 +115,6 @@ Every view has:
|
|
|
122
115
|
How to render frame with View?
|
|
123
116
|
```js
|
|
124
117
|
import { View } from '@nan0web/ui'
|
|
125
|
-
|
|
126
118
|
const view = new View()
|
|
127
119
|
view.render(1)(["Hello, world"])
|
|
128
120
|
console.info(String(view.frame)) // ← "\rHello, world"
|
|
@@ -141,38 +133,15 @@ Render methods:
|
|
|
141
133
|
How to create a Frame with fixed size?
|
|
142
134
|
```js
|
|
143
135
|
import { Frame } from '@nan0web/ui'
|
|
144
|
-
|
|
145
136
|
const frame = new Frame({
|
|
146
137
|
value: [["Frame content"]],
|
|
147
138
|
width: 20,
|
|
148
139
|
height: 5,
|
|
149
140
|
renderMethod: Frame.RenderMethod.APPEND,
|
|
150
141
|
})
|
|
151
|
-
|
|
152
142
|
const rendered = frame.render()
|
|
153
143
|
console.info(rendered.includes("Frame content")) // ← true
|
|
154
144
|
```
|
|
155
|
-
### App Architecture
|
|
156
|
-
|
|
157
|
-
`App` provides the main application logic.
|
|
158
|
-
|
|
159
|
-
- Core – minimal UI layer
|
|
160
|
-
- User – user-specific UI commands
|
|
161
|
-
|
|
162
|
-
Each app registers commands and binds them to UI actions.
|
|
163
|
-
|
|
164
|
-
How to create a basic user app that greets?
|
|
165
|
-
```js
|
|
166
|
-
import { App, View } from '@nan0web/ui'
|
|
167
|
-
|
|
168
|
-
const app = new App.User.App({ name: "GreetApp" })
|
|
169
|
-
const view = new View()
|
|
170
|
-
view.register("Welcome", Welcome)
|
|
171
|
-
|
|
172
|
-
const cmd = App.Command.Message.parse("welcome --user Bob")
|
|
173
|
-
const result = await app.processCommand(cmd, new App.User.UI(app, view))
|
|
174
|
-
console.info(String(result)) // ← Welcome Bob!
|
|
175
|
-
```
|
|
176
145
|
### Models
|
|
177
146
|
|
|
178
147
|
UI models are plain data objects managed by `Model` classes.
|
|
@@ -182,7 +151,6 @@ UI models are plain data objects managed by `Model` classes.
|
|
|
182
151
|
How to use a User model?
|
|
183
152
|
```js
|
|
184
153
|
import { Model } from '@nan0web/ui'
|
|
185
|
-
|
|
186
154
|
const user = new Model.User({ name: "Charlie", email: "charlie@example.com" })
|
|
187
155
|
console.info(user.name) // ← Charlie
|
|
188
156
|
console.info(user.email) // ← charlie@example.com
|
|
@@ -197,7 +165,6 @@ with minimal setup.
|
|
|
197
165
|
How to test UI components with assertions?
|
|
198
166
|
```js
|
|
199
167
|
import { Welcome, InputMessage } from '@nan0web/ui'
|
|
200
|
-
|
|
201
168
|
const output = Welcome({ user: { name: "Test" } })
|
|
202
169
|
const input = InputMessage.from({ value: "test" })
|
|
203
170
|
console.log(output[0].join("")) // ← Welcome Test!
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nan0web/ui",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "NaN•Web UI. One application logic (algorithm) and many UI.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "types/index.d.ts",
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"scripts": {
|
|
14
14
|
"build": "tsc",
|
|
15
15
|
"playground": "node playground/main.js",
|
|
16
|
-
"test": "node --test --test-timeout=3333 \"src/**/*.test.js\"
|
|
16
|
+
"test": "node --test --test-timeout=3333 \"src/**/*.test.js\"",
|
|
17
|
+
"test:nan0test": "node --test --test-timeout=3333 \"src/**/*.test.js\" | nan0test parse --fail",
|
|
17
18
|
"test:coverage": "node --experimental-test-coverage --test-coverage-include=\"src/**/*.js\" --test-coverage-exclude=\"src/**/*.test.js\" --test \"src/**/*.test.js\"",
|
|
18
19
|
"test:coverage:collect": "nan0test coverage",
|
|
19
20
|
"test:docs": "node --test src/README.md.js",
|
|
@@ -35,6 +36,10 @@
|
|
|
35
36
|
"import": "./src/Component/index.js",
|
|
36
37
|
"types": "./types/Component/index.d.ts"
|
|
37
38
|
},
|
|
39
|
+
"./core": {
|
|
40
|
+
"import": "./src/core/index.js",
|
|
41
|
+
"types": "./types/core/index.d.ts"
|
|
42
|
+
},
|
|
38
43
|
"./cli-app": "./apps/cli/src/index.js",
|
|
39
44
|
"./mobile-app": "./apps/mobile/src/index.js",
|
|
40
45
|
"./web-app": "./apps/web/src/App.jsx"
|
package/src/App/Core/CoreApp.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { Message } from "@nan0web/co"
|
|
1
2
|
import { typeOf } from "@nan0web/types"
|
|
2
|
-
import { CommandMessage } from "../Command/index.js"
|
|
3
3
|
import UI from "./UI.js"
|
|
4
4
|
|
|
5
5
|
/** @typedef {Function} CommandFn */
|
|
@@ -18,7 +18,7 @@ export default class CoreApp {
|
|
|
18
18
|
/** @type {object} App state */
|
|
19
19
|
state
|
|
20
20
|
|
|
21
|
-
/** @type {
|
|
21
|
+
/** @type {Message} Starting command parsed from argv */
|
|
22
22
|
startCommand
|
|
23
23
|
|
|
24
24
|
/**
|
|
@@ -26,18 +26,19 @@ export default class CoreApp {
|
|
|
26
26
|
* @param {object} props - CoreApp properties
|
|
27
27
|
* @param {string} [props.name="CoreApp"] - App name
|
|
28
28
|
* @param {object} [props.state={}] - Initial state object
|
|
29
|
-
* @param {
|
|
29
|
+
* @param {Message} [props.startCommand=new Message()] - Command line arguments to parse
|
|
30
30
|
*/
|
|
31
31
|
constructor(props = {}) {
|
|
32
32
|
const {
|
|
33
33
|
name = "CoreApp",
|
|
34
34
|
state = {},
|
|
35
|
-
|
|
35
|
+
startCommand = new Message(),
|
|
36
36
|
} = props
|
|
37
37
|
this.name = String(name)
|
|
38
38
|
this.state = state
|
|
39
39
|
this.commands = new Map()
|
|
40
|
-
|
|
40
|
+
// @deprecated @todo fix the argv by moving to ui-cli.
|
|
41
|
+
this.startCommand = Message.from(startCommand ?? {})
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
/**
|
|
@@ -78,39 +79,33 @@ export default class CoreApp {
|
|
|
78
79
|
|
|
79
80
|
/**
|
|
80
81
|
* Process a command message.
|
|
81
|
-
* @param {
|
|
82
|
+
* @param {Message} msg - Command to process
|
|
82
83
|
* @param {UI} ui - UI instance to use for rendering
|
|
83
84
|
* @returns {Promise<any>} Output of the command
|
|
84
85
|
* @throws {Error} If the command is not registered
|
|
85
86
|
*/
|
|
86
|
-
async processCommand(
|
|
87
|
-
const
|
|
88
|
-
const handler = this.commands.get(cmd)
|
|
87
|
+
async processCommand(msg, ui) {
|
|
88
|
+
const handler = this.commands.get(msg.constructor.name)
|
|
89
89
|
if (!handler) {
|
|
90
90
|
throw new Error([
|
|
91
91
|
"Unknown command", ": ",
|
|
92
|
-
|
|
92
|
+
msg.constructor.name, "\n",
|
|
93
93
|
"Available commands", ": ",
|
|
94
94
|
[...this.commands.keys()].join(", "),
|
|
95
95
|
].join(""))
|
|
96
96
|
}
|
|
97
|
-
|
|
98
|
-
const command = Class.from({
|
|
99
|
-
args: commandMessage.args.slice(1),
|
|
100
|
-
opts: commandMessage.opts,
|
|
101
|
-
})
|
|
102
|
-
return await handler.apply(this, [command, ui])
|
|
97
|
+
return await handler.apply(this, [msg, ui])
|
|
103
98
|
}
|
|
104
99
|
|
|
105
100
|
/**
|
|
106
101
|
* Process an array of command messages sequentially.
|
|
107
|
-
* @param {
|
|
102
|
+
* @param {Message[]} Messages - Array of commands to process
|
|
108
103
|
* @param {UI} ui - UI instance to use for rendering
|
|
109
104
|
* @returns {Promise<any[]>} Array of command outputs
|
|
110
105
|
*/
|
|
111
|
-
async processCommands(
|
|
106
|
+
async processCommands(Messages, ui) {
|
|
112
107
|
const results = []
|
|
113
|
-
for (const cmdMsg of
|
|
108
|
+
for (const cmdMsg of Messages) {
|
|
114
109
|
const result = await this.processCommand(cmdMsg, ui)
|
|
115
110
|
results.push(result)
|
|
116
111
|
}
|
package/src/App/Core/UI.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { Message } from "@nan0web/co"
|
|
2
|
+
import { notEmpty } from "@nan0web/types"
|
|
1
3
|
import View from "../../View/View.js"
|
|
2
4
|
import CoreApp from "./CoreApp.js"
|
|
3
5
|
import Widget from "./Widget.js"
|
|
4
|
-
import { notEmpty } from "@nan0web/types"
|
|
5
|
-
import { CommandMessage } from "../Command/index.js"
|
|
6
6
|
|
|
7
7
|
/** @typedef {import("../../View/View.js").ComponentFn} ComponentFn */
|
|
8
8
|
|
|
@@ -25,10 +25,10 @@ class UI extends Widget {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
* Convert raw input to
|
|
28
|
+
* Convert raw input to Message array.
|
|
29
29
|
* Must be implemented by subclasses.
|
|
30
30
|
* @param {any} rawInput - Raw input to convert
|
|
31
|
-
* @returns {
|
|
31
|
+
* @returns {Message[]} Array of command messages
|
|
32
32
|
* @throws {Error} Always thrown as this method must be implemented by subclasses
|
|
33
33
|
*/
|
|
34
34
|
convertInput(rawInput) {
|
|
@@ -58,7 +58,7 @@ class UI extends Widget {
|
|
|
58
58
|
const answer = await this.app.selectCommand(this)
|
|
59
59
|
if (answer) {
|
|
60
60
|
const [input] = this.convertInput(rawInput)
|
|
61
|
-
const cmd = new
|
|
61
|
+
const cmd = new Message({
|
|
62
62
|
argv: [answer],
|
|
63
63
|
opts: input?.opts ?? {},
|
|
64
64
|
})
|
package/src/App/User/UserApp.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { Message } from "@nan0web/co"
|
|
1
2
|
import { notEmpty } from "@nan0web/types"
|
|
2
3
|
import CoreApp from "../Core/CoreApp.js"
|
|
3
|
-
import Command, { CommandMessage } from "./Command/index.js"
|
|
4
4
|
import User from "../../Model/User/User.js"
|
|
5
5
|
import UserUI from "./UserUI.js"
|
|
6
6
|
import InputMessage from "../../core/Message/InputMessage.js"
|
|
@@ -11,34 +11,19 @@ import InputMessage from "../../core/Message/InputMessage.js"
|
|
|
11
11
|
* User can change user data to see another Welcome view.
|
|
12
12
|
*/
|
|
13
13
|
class UserApp extends CoreApp {
|
|
14
|
-
/** @type {CommandMessage} Starting command parsed from argv */
|
|
15
|
-
startCommand
|
|
16
|
-
|
|
17
|
-
/** @type {object} App state */
|
|
18
|
-
state
|
|
19
|
-
|
|
20
14
|
/**
|
|
21
15
|
* Creates a new UserApp instance.
|
|
22
|
-
* @param {
|
|
23
|
-
* @param {string} [props.name="UserApp"] - App name
|
|
24
|
-
* @param {object} [props.state={}] - Initial state object
|
|
25
|
-
* @param {string[]} [props.argv=[]] - Command line arguments to parse
|
|
16
|
+
* @param {Partial<CoreApp>} [props={}] - UserApp properties
|
|
26
17
|
*/
|
|
27
18
|
constructor(props = {}) {
|
|
28
19
|
super(props)
|
|
29
|
-
const {
|
|
30
|
-
argv = [],
|
|
31
|
-
state = {},
|
|
32
|
-
} = props
|
|
33
|
-
this.state = state
|
|
34
|
-
this.startCommand = CommandMessage.parse(argv)
|
|
35
20
|
this.registerCommand("setUser", this.setUser.bind(this))
|
|
36
21
|
this.registerCommand("welcome", this.welcome.bind(this))
|
|
37
22
|
}
|
|
38
23
|
|
|
39
24
|
/**
|
|
40
25
|
* Set user data from params.
|
|
41
|
-
* @param {
|
|
26
|
+
* @param {Message} cmd - Command message with user data
|
|
42
27
|
* @param {UserUI} ui - UI instance
|
|
43
28
|
* @returns {Promise<{ message: string }>} Welcome message
|
|
44
29
|
*/
|
|
@@ -52,7 +37,7 @@ class UserApp extends CoreApp {
|
|
|
52
37
|
|
|
53
38
|
/**
|
|
54
39
|
* Show welcome message for current user.
|
|
55
|
-
* @param {
|
|
40
|
+
* @param {Message} cmd - Command message
|
|
56
41
|
* @param {UserUI} ui - UI instance
|
|
57
42
|
* @returns {Promise<string[][]>} Welcome view output
|
|
58
43
|
*/
|
package/src/App/User/UserUI.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { Message } from "@nan0web/co"
|
|
1
2
|
import App from "../Core/index.js"
|
|
2
|
-
import { CommandMessage } from "./Command/index.js"
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* UserUI connects UserApp and View.
|
|
@@ -8,13 +8,13 @@ import { CommandMessage } from "./Command/index.js"
|
|
|
8
8
|
*/
|
|
9
9
|
export default class UserUI extends App.UI {
|
|
10
10
|
/**
|
|
11
|
-
* Convert raw input to
|
|
11
|
+
* Convert raw input to Message array.
|
|
12
12
|
* If user.name provided in rawInput, use it directly.
|
|
13
13
|
* Otherwise ask user for name.
|
|
14
14
|
* @param {any} rawInput - Raw input to convert
|
|
15
|
-
* @returns {
|
|
15
|
+
* @returns {Message[]} Array of command messages
|
|
16
16
|
*/
|
|
17
17
|
convertInput(rawInput) {
|
|
18
|
-
return [
|
|
18
|
+
return [new Message({ body: rawInput })]
|
|
19
19
|
}
|
|
20
20
|
}
|
package/src/App/User/index.js
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
import UserApp from "./UserApp.js"
|
|
2
2
|
import UserUI from "./UserUI.js"
|
|
3
|
-
import UserCommand from "./Command/index.js"
|
|
4
|
-
import Command from "../Command/index.js"
|
|
5
3
|
|
|
6
4
|
export { UserApp, UserUI }
|
|
7
5
|
|
|
8
6
|
export default {
|
|
9
7
|
App: UserApp,
|
|
10
8
|
UI: UserUI,
|
|
11
|
-
Command: {
|
|
12
|
-
...Command,
|
|
13
|
-
...UserCommand,
|
|
14
|
-
},
|
|
15
9
|
}
|
package/src/App/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import Command from "./Command/index.js"
|
|
2
1
|
import Scenario from "./Scenario.js"
|
|
3
2
|
import UI from "./Core/UI.js"
|
|
4
3
|
|
|
@@ -8,7 +7,6 @@ import User from "./User/index.js"
|
|
|
8
7
|
export {
|
|
9
8
|
Core,
|
|
10
9
|
User,
|
|
11
|
-
Command,
|
|
12
10
|
Scenario,
|
|
13
11
|
UI,
|
|
14
12
|
}
|
|
@@ -16,7 +14,6 @@ export {
|
|
|
16
14
|
export default {
|
|
17
15
|
Core,
|
|
18
16
|
User,
|
|
19
|
-
Command,
|
|
20
17
|
Scenario,
|
|
21
18
|
UI,
|
|
22
19
|
}
|
package/src/README.md.js
CHANGED
|
@@ -120,10 +120,10 @@ function testRender() {
|
|
|
120
120
|
|
|
121
121
|
const input = InputMessage.from({ value: 'Hello User' })
|
|
122
122
|
const output = OutputMessage.from({ content: ['Welcome to @nan0web/ui'] })
|
|
123
|
-
console.info(input.value) // ← Hello User
|
|
123
|
+
console.info(input.value) // ← Message { body: "Hello User", head: {} }
|
|
124
124
|
console.info(output.content[0]) // ← Welcome to @nan0web/ui
|
|
125
|
-
assert.
|
|
126
|
-
assert.equal(output
|
|
125
|
+
assert.deepStrictEqual({ ...console.output()[0][1] }, { body: "Hello User", head: {} })
|
|
126
|
+
assert.equal(console.output()[1][1], 'Welcome to @nan0web/ui')
|
|
127
127
|
})
|
|
128
128
|
|
|
129
129
|
/**
|
|
@@ -249,30 +249,6 @@ function testRender() {
|
|
|
249
249
|
assert.ok(renderedVisible.includes("Frame content"))
|
|
250
250
|
})
|
|
251
251
|
|
|
252
|
-
/**
|
|
253
|
-
* @docs
|
|
254
|
-
* ### App Architecture
|
|
255
|
-
*
|
|
256
|
-
* `App` provides the main application logic.
|
|
257
|
-
*
|
|
258
|
-
* - Core – minimal UI layer
|
|
259
|
-
* - User – user-specific UI commands
|
|
260
|
-
*
|
|
261
|
-
* Each app registers commands and binds them to UI actions.
|
|
262
|
-
*/
|
|
263
|
-
it("How to create a basic user app that greets?", async () => {
|
|
264
|
-
//import { App, View } from '@nan0web/ui'
|
|
265
|
-
|
|
266
|
-
const app = new App.User.App({ name: "GreetApp" })
|
|
267
|
-
const view = new View()
|
|
268
|
-
view.register("Welcome", Welcome)
|
|
269
|
-
|
|
270
|
-
const cmd = App.Command.Message.parse("welcome --user Bob")
|
|
271
|
-
const result = await app.processCommand(cmd, new App.User.UI(app, view))
|
|
272
|
-
console.info(String(result)) // ← Welcome Bob!
|
|
273
|
-
assert.ok(console.output()[0][1].includes("Welcome Bob!"))
|
|
274
|
-
})
|
|
275
|
-
|
|
276
252
|
/**
|
|
277
253
|
* @docs
|
|
278
254
|
* ### Models
|
package/src/StdIn.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import EventProcessor from "@nan0web/event/oop"
|
|
2
|
-
import { CommandMessage } from "@nan0web/co"
|
|
3
2
|
import { typeOf } from "@nan0web/types"
|
|
4
3
|
import InputMessage from "./core/Message/InputMessage.js"
|
|
5
4
|
|
|
@@ -91,8 +90,7 @@ class StdIn extends EventProcessor {
|
|
|
91
90
|
decode(message) {
|
|
92
91
|
if (message instanceof InputMessage) return message
|
|
93
92
|
if (Array.isArray(message) && message.every(typeOf(String))) {
|
|
94
|
-
|
|
95
|
-
return new InputMessage({ value: parsed })
|
|
93
|
+
return new InputMessage({ value: message })
|
|
96
94
|
}
|
|
97
95
|
return new InputMessage(message)
|
|
98
96
|
}
|
package/src/core/Form/Form.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import FormMessage from "./Message.js"
|
|
2
2
|
import FormInput from "./Input.js"
|
|
3
|
+
import Message from "@nan0web/co"
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Abstract form for data entry.
|
|
@@ -18,24 +19,24 @@ export default class UIForm extends FormMessage {
|
|
|
18
19
|
/** @type {Object} */ schema = {}
|
|
19
20
|
|
|
20
21
|
/* ------------------------------------------------------------------ */
|
|
21
|
-
/* static
|
|
22
|
+
/* static validation registry */
|
|
22
23
|
/* ------------------------------------------------------------------ */
|
|
23
24
|
|
|
24
25
|
/** @type {Object<string,Function>} */
|
|
25
|
-
static
|
|
26
|
+
static _validations = {}
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
|
-
* Register a custom
|
|
29
|
+
* Register a custom validation that can be referenced by name in a schema.
|
|
29
30
|
*
|
|
30
|
-
* @param {string} name - Identifier used in schema.
|
|
31
|
+
* @param {string} name - Identifier used in schema.validation.
|
|
31
32
|
* @param {(value:any)=>true|string} fn - Function returns true if valid,
|
|
32
33
|
* otherwise returns an error message.
|
|
33
34
|
*/
|
|
34
|
-
static
|
|
35
|
+
static addValidation(name, fn) {
|
|
35
36
|
if (typeof name !== "string" || typeof fn !== "function") {
|
|
36
|
-
throw new Error("
|
|
37
|
+
throw new Error("validation name must be a string and fn must be a function")
|
|
37
38
|
}
|
|
38
|
-
UIForm.
|
|
39
|
+
UIForm._validations[name] = fn
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
/**
|
|
@@ -208,15 +209,15 @@ export default class UIForm extends FormMessage {
|
|
|
208
209
|
}
|
|
209
210
|
}
|
|
210
211
|
|
|
211
|
-
// Custom
|
|
212
|
-
if (schema.
|
|
212
|
+
// Custom validation – can be a function or a string referencing a static validation
|
|
213
|
+
if (schema.validation) {
|
|
213
214
|
let result
|
|
214
|
-
if (typeof schema.
|
|
215
|
-
result = schema.
|
|
216
|
-
} else if (typeof schema.
|
|
217
|
-
const fn = UIForm.
|
|
215
|
+
if (typeof schema.validation === 'function') {
|
|
216
|
+
result = schema.validation(value)
|
|
217
|
+
} else if (typeof schema.validation === 'string') {
|
|
218
|
+
const fn = UIForm._validations[schema.validation]
|
|
218
219
|
if (!fn) {
|
|
219
|
-
throw new Error(`
|
|
220
|
+
throw new Error(`validation "${schema.validation}" not registered`)
|
|
220
221
|
}
|
|
221
222
|
result = fn(value)
|
|
222
223
|
}
|
|
@@ -236,9 +237,7 @@ export default class UIForm extends FormMessage {
|
|
|
236
237
|
*/
|
|
237
238
|
toJSON() {
|
|
238
239
|
return {
|
|
239
|
-
|
|
240
|
-
type: this.type,
|
|
241
|
-
time: this.time.toISOString(),
|
|
240
|
+
time: new Date(this.time).toISOString(),
|
|
242
241
|
title: this.title,
|
|
243
242
|
fields: this.fields.map(f => f.toJSON ? f.toJSON() : f),
|
|
244
243
|
state: this.state,
|
|
@@ -252,6 +251,25 @@ export default class UIForm extends FormMessage {
|
|
|
252
251
|
*/
|
|
253
252
|
static from(input) {
|
|
254
253
|
if (input instanceof UIForm) return input
|
|
254
|
+
if (input instanceof Message) {
|
|
255
|
+
const Class = input.constructor
|
|
256
|
+
const fields = []
|
|
257
|
+
for (const [name, value] of Object.entries(input)) {
|
|
258
|
+
fields.push(new FormInput({
|
|
259
|
+
name,
|
|
260
|
+
label: Class[name]?.label ?? Class[`${name}Label`] ?? name,
|
|
261
|
+
type: Class[name]?.type ?? Class[`${name}Type`] ?? typeof value,
|
|
262
|
+
required: Class[name]?.required ?? Class[`${name}Required`] ?? false,
|
|
263
|
+
placeholder: Class[name]?.placeholder ?? Class[`${name}Placeholder`] ?? "",
|
|
264
|
+
defaultValue: Class[name]?.defaultValue ?? Class[`${name}Default`] ?? "",
|
|
265
|
+
validation: Class[name]?.validation ?? Class[`${name}Validation`] ?? (() => true),
|
|
266
|
+
}))
|
|
267
|
+
}
|
|
268
|
+
return new UIForm({
|
|
269
|
+
title: Class.name,
|
|
270
|
+
fields
|
|
271
|
+
})
|
|
272
|
+
}
|
|
255
273
|
return new UIForm(input)
|
|
256
274
|
}
|
|
257
275
|
|
|
@@ -280,7 +298,7 @@ export default class UIForm extends FormMessage {
|
|
|
280
298
|
required: !!custom.required,
|
|
281
299
|
placeholder: custom.placeholder ?? "",
|
|
282
300
|
options: custom.options ?? [],
|
|
283
|
-
|
|
301
|
+
validation: custom.validation ?? undefined,
|
|
284
302
|
defaultValue: custom.defaultValue ?? "",
|
|
285
303
|
})
|
|
286
304
|
})
|
package/src/core/Form/Input.js
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} Filter
|
|
3
|
+
* @property {string} [q=""]
|
|
4
|
+
* @property {number} [offset=0]
|
|
5
|
+
* @property {number} [limit=36]
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** @typedef {Array<string> | ((filter: Filter) => Promise<string[]>)} InputOptions */
|
|
9
|
+
|
|
1
10
|
/**
|
|
2
11
|
* Form input field descriptor.
|
|
3
12
|
*
|
|
@@ -8,7 +17,7 @@
|
|
|
8
17
|
* @property {boolean} required - Whether the field is required.
|
|
9
18
|
* @property {string} placeholder - Placeholder text.
|
|
10
19
|
* @property {Array<string>} options - Select options (if type is 'select').
|
|
11
|
-
* @property {Function|null}
|
|
20
|
+
* @property {Function|null} validation - Custom validation function.
|
|
12
21
|
* @property {*} defaultValue - Default value.
|
|
13
22
|
*/
|
|
14
23
|
export default class FormInput {
|
|
@@ -17,8 +26,8 @@ export default class FormInput {
|
|
|
17
26
|
/** @type {string} */ type = 'text'
|
|
18
27
|
/** @type {boolean} */ required = false
|
|
19
28
|
/** @type {string} */ placeholder = ''
|
|
20
|
-
/** @type {
|
|
21
|
-
/** @type {Function|null} */
|
|
29
|
+
/** @type {InputOptions} */ options = []
|
|
30
|
+
/** @type {Function|null} */ validation = null
|
|
22
31
|
/** @type {*} */ defaultValue = null
|
|
23
32
|
|
|
24
33
|
/**
|
|
@@ -42,8 +51,8 @@ export default class FormInput {
|
|
|
42
51
|
* @param {string} [props.type='text'] - Input type.
|
|
43
52
|
* @param {boolean} [props.required=false] - Is required.
|
|
44
53
|
* @param {string} [props.placeholder=''] - Placeholder.
|
|
45
|
-
* @param {
|
|
46
|
-
* @param {Function} [props.
|
|
54
|
+
* @param {InputOptions} [props.options=[]] - Select options or async function to retrieve data with the search and page.
|
|
55
|
+
* @param {Function} [props.validation=null] - Custom validation.
|
|
47
56
|
* @param {*} [props.defaultValue=null] - Default value.
|
|
48
57
|
*/
|
|
49
58
|
constructor(props) {
|
|
@@ -54,7 +63,7 @@ export default class FormInput {
|
|
|
54
63
|
required = this.required,
|
|
55
64
|
placeholder = this.placeholder,
|
|
56
65
|
options = [],
|
|
57
|
-
|
|
66
|
+
validation = this.validation,
|
|
58
67
|
defaultValue = this.defaultValue
|
|
59
68
|
} = props
|
|
60
69
|
|
|
@@ -68,7 +77,7 @@ export default class FormInput {
|
|
|
68
77
|
this.required = Boolean(required)
|
|
69
78
|
this.placeholder = String(placeholder)
|
|
70
79
|
this.options = options
|
|
71
|
-
this.
|
|
80
|
+
this.validation = validation
|
|
72
81
|
this.defaultValue = defaultValue
|
|
73
82
|
|
|
74
83
|
this.requireValidType()
|
package/src/core/Form/Message.js
CHANGED
|
@@ -1,22 +1,19 @@
|
|
|
1
|
-
import
|
|
1
|
+
import InputMessage from "../Message/InputMessage.js"
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* FormMessage – specialized
|
|
4
|
+
* FormMessage – specialized InputMessage for forms.
|
|
5
5
|
*
|
|
6
6
|
* @class FormMessage
|
|
7
|
-
* @extends
|
|
7
|
+
* @extends InputMessage
|
|
8
8
|
*/
|
|
9
|
-
export default class FormMessage extends
|
|
9
|
+
export default class FormMessage extends InputMessage {
|
|
10
10
|
/**
|
|
11
11
|
* Creates a FormMessage.
|
|
12
12
|
*
|
|
13
13
|
* @param {Object} [input={}] - Message properties.
|
|
14
14
|
*/
|
|
15
15
|
constructor(input = {}) {
|
|
16
|
-
super(
|
|
17
|
-
...input,
|
|
18
|
-
type: OutputMessage.TYPES.FORM,
|
|
19
|
-
})
|
|
16
|
+
super(input)
|
|
20
17
|
const {
|
|
21
18
|
data = {},
|
|
22
19
|
schema = {},
|
|
@@ -83,4 +80,4 @@ export default class FormMessage extends OutputMessage {
|
|
|
83
80
|
|
|
84
81
|
return { isValid: Object.keys(errors).length === 0, errors }
|
|
85
82
|
}
|
|
86
|
-
}
|
|
83
|
+
}
|
package/src/core/InputAdapter.js
CHANGED
|
@@ -10,6 +10,10 @@ import CancelError from "./Error/CancelError.js"
|
|
|
10
10
|
*/
|
|
11
11
|
export default class InputAdapter extends Event {
|
|
12
12
|
static CancelError = CancelError
|
|
13
|
+
/** @returns {typeof CancelError} */
|
|
14
|
+
get CancelError() {
|
|
15
|
+
return /** @type {typeof InputAdapter} */ (this.constructor).CancelError
|
|
16
|
+
}
|
|
13
17
|
/**
|
|
14
18
|
* Starts listening for input and emits an `input` event.
|
|
15
19
|
*
|