@nan0web/ui 1.0.3 → 1.0.4
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 +18 -20
- package/package.json +15 -16
- package/src/App/Core/UI.js +0 -46
- package/src/App/Core/Widget.js +4 -6
- package/src/App/User/Command/Message.js +23 -37
- package/src/App/User/Command/index.js +3 -8
- package/src/App/User/UserApp.js +30 -10
- package/src/README.md.js +33 -33
- package/src/StdIn.js +12 -13
- package/src/View/View.js +5 -5
- package/src/core/Form/Form.js +8 -6
- package/src/core/Form/Input.js +1 -1
- package/src/core/Form/Message.js +6 -5
- package/src/core/InputAdapter.js +2 -2
- package/src/core/Message/Message.js +109 -19
- package/src/core/Message/OutputMessage.js +7 -7
- package/src/core/Message/index.js +3 -4
- package/src/core/Stream.js +10 -10
- package/src/core/UiAdapter.js +189 -0
- package/src/core/index.js +3 -6
- package/src/index.js +4 -4
- package/types/App/Core/UI.d.ts +0 -10
- package/types/App/Core/Widget.d.ts +6 -7
- package/types/App/User/Command/Message.d.ts +15 -28
- package/types/App/User/Command/index.d.ts +3 -4
- package/types/App/User/UserApp.d.ts +14 -7
- package/types/StdIn.d.ts +13 -13
- package/types/View/View.d.ts +6 -6
- package/types/core/Form/Form.d.ts +2 -5
- package/types/core/Form/Input.d.ts +1 -1
- package/types/core/Form/Message.d.ts +5 -10
- package/types/core/Intent.d.ts +91 -0
- package/types/core/Message/Message.d.ts +58 -15
- package/types/core/Message/OutputMessage.d.ts +3 -3
- package/types/core/Message/index.d.ts +3 -4
- package/types/core/Stream.d.ts +5 -4
- package/types/core/UiAdapter.d.ts +104 -0
- package/types/core/index.d.ts +2 -3
- package/types/index.d.ts +4 -4
- package/src/App/User/Command/Options.js +0 -48
- package/src/core/Message/InputMessage.js +0 -119
package/README.md
CHANGED
|
@@ -41,8 +41,7 @@ yarn add @nan0web/ui
|
|
|
41
41
|
|
|
42
42
|
UI communication is built around messages:
|
|
43
43
|
|
|
44
|
-
- **`
|
|
45
|
-
- **`InputMessage`** – user input message (value, options)
|
|
44
|
+
- **`UiMessage`** – abstract message base class
|
|
46
45
|
- **`OutputMessage`** – system output (content, error, priority)
|
|
47
46
|
|
|
48
47
|
Messages are simple, serializable data containers. They help build
|
|
@@ -51,14 +50,14 @@ decoupled communication systems between UI components.
|
|
|
51
50
|
How to create input and output messages?
|
|
52
51
|
```js
|
|
53
52
|
import { InputMessage, OutputMessage } from '@nan0web/ui'
|
|
54
|
-
const input =
|
|
53
|
+
const input = UiMessage.from({ body: 'Hello User' })
|
|
55
54
|
const output = OutputMessage.from({ content: ['Welcome to @nan0web/ui'] })
|
|
56
|
-
console.info(input
|
|
57
|
-
console.info(output
|
|
55
|
+
console.info(input) // ← Message { body: "Hello User", head: {}, id: "....", type: "" }
|
|
56
|
+
console.info(String(output)) // ← Welcome to @nan0web/ui
|
|
58
57
|
```
|
|
59
58
|
### Forms
|
|
60
59
|
|
|
61
|
-
`
|
|
60
|
+
`UiForm` supports field definitions, data management, and schema validation.
|
|
62
61
|
Every form includes a title, fields, and current state.
|
|
63
62
|
|
|
64
63
|
Field types include:
|
|
@@ -70,10 +69,10 @@ Field types include:
|
|
|
70
69
|
- `checkbox`
|
|
71
70
|
- `textarea`
|
|
72
71
|
|
|
73
|
-
How to define and validate a
|
|
72
|
+
How to define and validate a UiForm?
|
|
74
73
|
```js
|
|
75
|
-
import {
|
|
76
|
-
const form = new
|
|
74
|
+
import { UiForm } from '@nan0web/ui'
|
|
75
|
+
const form = new UiForm({
|
|
77
76
|
title: "Contact Form",
|
|
78
77
|
fields: [
|
|
79
78
|
FormInput.from({ name: "email", label: "Email Address", type: "email", required: true }),
|
|
@@ -84,9 +83,9 @@ const form = new UIForm({
|
|
|
84
83
|
message: "Hello!"
|
|
85
84
|
}
|
|
86
85
|
})
|
|
87
|
-
const
|
|
88
|
-
console.info(
|
|
89
|
-
console.info(
|
|
86
|
+
const errors = form.validate()
|
|
87
|
+
console.info(errors.size) // ← 1
|
|
88
|
+
console.info(errors.get("email")) // ← Invalid email format
|
|
90
89
|
```
|
|
91
90
|
### Components
|
|
92
91
|
|
|
@@ -164,19 +163,18 @@ with minimal setup.
|
|
|
164
163
|
|
|
165
164
|
How to test UI components with assertions?
|
|
166
165
|
```js
|
|
167
|
-
import { Welcome
|
|
166
|
+
import { Welcome } from '@nan0web/ui'
|
|
168
167
|
const output = Welcome({ user: { name: "Test" } })
|
|
169
|
-
|
|
170
|
-
console.log(output[0].join("")) // ← Welcome Test!
|
|
168
|
+
console.info(output) // ← Welcome Test!
|
|
171
169
|
```
|
|
172
170
|
## Playground Demos
|
|
173
171
|
|
|
174
172
|
The library includes rich playground demos:
|
|
175
173
|
|
|
176
|
-
- [Registration Form](./
|
|
177
|
-
- [Currency Exchange](./
|
|
178
|
-
- [Mobile Top-up](./
|
|
179
|
-
- [Language Selector](./
|
|
174
|
+
- [Registration Form](./play/registration.form.js)
|
|
175
|
+
- [Currency Exchange](./play/currency.exchange.js)
|
|
176
|
+
- [Mobile Top-up](./play/topup.telephone.js)
|
|
177
|
+
- [Language Selector](./play/language.form.js)
|
|
180
178
|
|
|
181
179
|
Run to explore live functionality:
|
|
182
180
|
|
|
@@ -186,7 +184,7 @@ How to run the playground?
|
|
|
186
184
|
git clone https://github.com/nan0web/ui.git
|
|
187
185
|
cd ui
|
|
188
186
|
npm install
|
|
189
|
-
npm run
|
|
187
|
+
npm run play
|
|
190
188
|
```
|
|
191
189
|
|
|
192
190
|
## API Documentation
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nan0web/ui",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
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",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
],
|
|
13
13
|
"scripts": {
|
|
14
14
|
"build": "tsc",
|
|
15
|
-
"
|
|
15
|
+
"play": "node play/main.js",
|
|
16
16
|
"test": "node --test --test-timeout=3333 \"src/**/*.test.js\"",
|
|
17
17
|
"test:nan0test": "node --test --test-timeout=3333 \"src/**/*.test.js\" | nan0test parse --fail",
|
|
18
18
|
"test:coverage": "node --experimental-test-coverage --test-coverage-include=\"src/**/*.js\" --test-coverage-exclude=\"src/**/*.test.js\" --test \"src/**/*.test.js\"",
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
"test:docs": "node --test src/README.md.js",
|
|
21
21
|
"test:release": "node --test \"releases/**/*.test.js\"",
|
|
22
22
|
"test:status": "nan0test status --hide-name",
|
|
23
|
+
"test:play": "node --test --test-timeout=3333 \"play/**/*.test.js\"",
|
|
24
|
+
"test:all": "npm run test && npm run test:docs && npm run test:play && npm run build",
|
|
23
25
|
"precommit": "npm test",
|
|
24
26
|
"prepush": "npm test",
|
|
25
27
|
"prepare": "husky",
|
|
@@ -52,23 +54,20 @@
|
|
|
52
54
|
"license": "ISC",
|
|
53
55
|
"packageManager": "pnpm@10.11.0",
|
|
54
56
|
"devDependencies": {
|
|
55
|
-
"@nan0web/
|
|
56
|
-
"@nan0web/
|
|
57
|
-
"@nan0web/
|
|
58
|
-
"@nan0web/
|
|
59
|
-
"@nan0web/
|
|
60
|
-
"@nan0web/types": "workspace:*",
|
|
61
|
-
"@nan0web/ui-cli": "workspace:*",
|
|
57
|
+
"@nan0web/event": "^1.0.0",
|
|
58
|
+
"@nan0web/i18n": "^1.0.1",
|
|
59
|
+
"@nan0web/release": "1.0.1",
|
|
60
|
+
"@nan0web/test": "1.1.0",
|
|
61
|
+
"@nan0web/ui-cli": "1.0.2",
|
|
62
62
|
"@vitest/coverage-v8": "^3.2.4",
|
|
63
|
-
"
|
|
64
|
-
"
|
|
63
|
+
"husky": "^9.1.7",
|
|
64
|
+
"vitest": "^3.2.4"
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
|
+
"@nan0web/co": "^2.0.0",
|
|
68
|
+
"@nan0web/event": "^1.0.0",
|
|
69
|
+
"@nan0web/types": "^1.2.0",
|
|
67
70
|
"string-width": "^7.2.0"
|
|
68
71
|
},
|
|
69
|
-
"peerDependencies": {
|
|
70
|
-
"@nan0web/co": "^1.0.0",
|
|
71
|
-
"@nan0web/event": "^1.0.0",
|
|
72
|
-
"@nan0web/types": "^1.0.0"
|
|
73
|
-
}
|
|
72
|
+
"peerDependencies": {}
|
|
74
73
|
}
|
package/src/App/Core/UI.js
CHANGED
|
@@ -35,52 +35,6 @@ class UI extends Widget {
|
|
|
35
35
|
throw new Error("convertInput must be implemented by subclass")
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
/**
|
|
39
|
-
* Process input, run commands on app, and output results.
|
|
40
|
-
* Supports progress callback.
|
|
41
|
-
* @emits {start} Emitted when processing begins
|
|
42
|
-
* @emits {data} Emitted for each command being processed
|
|
43
|
-
* @emits {end} Emitted when all commands have been processed
|
|
44
|
-
* @param {any} rawInput - Raw input to process
|
|
45
|
-
* @returns {Promise<any[]>} Results of command processing
|
|
46
|
-
*/
|
|
47
|
-
async process(rawInput) {
|
|
48
|
-
const commands = this.convertInput(rawInput).filter(notEmpty)
|
|
49
|
-
const results = []
|
|
50
|
-
let count = commands.length
|
|
51
|
-
|
|
52
|
-
const proc = this.view.get("UIProcess")
|
|
53
|
-
if (proc) {
|
|
54
|
-
this.show(proc)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (0 === count && this.app.selectCommand) {
|
|
58
|
-
const answer = await this.app.selectCommand(this)
|
|
59
|
-
if (answer) {
|
|
60
|
-
const [input] = this.convertInput(rawInput)
|
|
61
|
-
const cmd = new Message({
|
|
62
|
-
argv: [answer],
|
|
63
|
-
opts: input?.opts ?? {},
|
|
64
|
-
})
|
|
65
|
-
commands.push(cmd)
|
|
66
|
-
++count
|
|
67
|
-
} else {
|
|
68
|
-
this.emit("end", { commands, results })
|
|
69
|
-
return results
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
this.emit("start", { commands })
|
|
73
|
-
for (let i = 0; i < count; i++) {
|
|
74
|
-
const command = commands[i]
|
|
75
|
-
this.emit("data", { i, count, command })
|
|
76
|
-
const result = await this.app.processCommand(command, this)
|
|
77
|
-
results.push(result)
|
|
78
|
-
}
|
|
79
|
-
this.emit("end", { commands, results })
|
|
80
|
-
// this.output(results)
|
|
81
|
-
return results
|
|
82
|
-
}
|
|
83
|
-
|
|
84
38
|
/**
|
|
85
39
|
* Sets up event handlers for UI process events.
|
|
86
40
|
* @param {ComponentFn} UIProcess - Process view component
|
package/src/App/Core/Widget.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import EventProcessor from "@nan0web/event/oop"
|
|
2
2
|
import View from "../../View/View.js"
|
|
3
|
-
import InputMessage from "../../core/Message/InputMessage.js"
|
|
4
3
|
import { StreamEntry } from "@nan0web/db"
|
|
4
|
+
import { UiMessage } from "../../core/index.js"
|
|
5
5
|
|
|
6
6
|
/** @typedef {import("./UI.js").ComponentFn} ComponentFn */
|
|
7
7
|
|
|
@@ -10,7 +10,7 @@ import { StreamEntry } from "@nan0web/db"
|
|
|
10
10
|
* Widget is a view with ability to input data in a specific format.
|
|
11
11
|
* Input and output data are typed classes.
|
|
12
12
|
*/
|
|
13
|
-
class Widget extends EventProcessor {
|
|
13
|
+
export default class Widget extends EventProcessor {
|
|
14
14
|
/** @type {View} The view associated with this widget */
|
|
15
15
|
view
|
|
16
16
|
|
|
@@ -25,8 +25,8 @@ class Widget extends EventProcessor {
|
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* Ask user for input data of specific class.
|
|
28
|
-
* @param {
|
|
29
|
-
* @returns {Promise<
|
|
28
|
+
* @param {UiMessage} input - instance of UiMessage or similar
|
|
29
|
+
* @returns {Promise<UiMessage | null>} instance of UiMessage or null
|
|
30
30
|
*/
|
|
31
31
|
async ask(input) {
|
|
32
32
|
return await this.view.ask(input)
|
|
@@ -63,5 +63,3 @@ class Widget extends EventProcessor {
|
|
|
63
63
|
return this.view.render(viewFn)(outputData)
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
-
|
|
67
|
-
export default Widget
|
|
@@ -1,44 +1,30 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import Message from "@nan0web/co"
|
|
2
|
+
import UIMessage from "../../../core/Message/Message.js"
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
* Creates a new UserAppCommandMessage instance.
|
|
10
|
-
* @param {object} props - Command message properties
|
|
11
|
-
* @param {string[]} [props.args=[]] - Command arguments
|
|
12
|
-
* @param {Partial<UserAppCommandOptions>} [props.opts={}] - User-specific options
|
|
13
|
-
*/
|
|
14
|
-
constructor(props = {}) {
|
|
15
|
-
super(props)
|
|
4
|
+
class DepsCommandParams {
|
|
5
|
+
fix = false
|
|
6
|
+
static fix = {
|
|
7
|
+
help: "Fix dependencies",
|
|
8
|
+
defaultValue: false
|
|
16
9
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* @param {Partial<UserAppCommandOptions>} value
|
|
25
|
-
*/
|
|
26
|
-
set opts(value) {
|
|
27
|
-
super.opts = UserAppCommandOptions.from(value)
|
|
10
|
+
constructor(input = {}) {
|
|
11
|
+
const {
|
|
12
|
+
fix = this.fix
|
|
13
|
+
} = input
|
|
28
14
|
}
|
|
15
|
+
}
|
|
29
16
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return new this(result)
|
|
17
|
+
export class DepsCommand extends UIMessage {
|
|
18
|
+
static Body = DepsCommandParams
|
|
19
|
+
/** @type {DepsCommandParams} */
|
|
20
|
+
body
|
|
21
|
+
constructor(input = {}) {
|
|
22
|
+
const {
|
|
23
|
+
body = new DepsCommandParams()
|
|
24
|
+
} = UIMessage.parseBody(input, DepsCommandParams)
|
|
25
|
+
super(input)
|
|
26
|
+
this.body = body
|
|
41
27
|
}
|
|
42
28
|
}
|
|
43
29
|
|
|
44
|
-
export default
|
|
30
|
+
export default DepsCommand
|
|
@@ -1,11 +1,6 @@
|
|
|
1
|
-
import Command from "../../Command/index.js"
|
|
2
1
|
import CommandMessage from "./Message.js"
|
|
3
|
-
import
|
|
2
|
+
import DepsCommand from "./Message.js"
|
|
4
3
|
|
|
5
|
-
export { CommandMessage,
|
|
4
|
+
export { CommandMessage, DepsCommand }
|
|
6
5
|
|
|
7
|
-
export default
|
|
8
|
-
...Command,
|
|
9
|
-
Message: CommandMessage,
|
|
10
|
-
Options: CommandOptions,
|
|
11
|
-
}
|
|
6
|
+
export default CommandMessage
|
package/src/App/User/UserApp.js
CHANGED
|
@@ -3,14 +3,17 @@ import { notEmpty } from "@nan0web/types"
|
|
|
3
3
|
import CoreApp from "../Core/CoreApp.js"
|
|
4
4
|
import User from "../../Model/User/User.js"
|
|
5
5
|
import UserUI from "./UserUI.js"
|
|
6
|
-
import
|
|
6
|
+
import UserAppCommandMessage from "./Command/Message.js"
|
|
7
|
+
import DepsCommand from "./Command/Message.js"
|
|
8
|
+
import UIStream from "../../core/Stream.js"
|
|
9
|
+
import { UiMessage } from "../../core/index.js"
|
|
7
10
|
|
|
8
11
|
/**
|
|
9
12
|
* UserApp requires user name and shows Welcome view.
|
|
10
13
|
* If user.name is provided in command input, ignores user input.
|
|
11
14
|
* User can change user data to see another Welcome view.
|
|
12
15
|
*/
|
|
13
|
-
class UserApp extends CoreApp {
|
|
16
|
+
export default class UserApp extends CoreApp {
|
|
14
17
|
/**
|
|
15
18
|
* Creates a new UserApp instance.
|
|
16
19
|
* @param {Partial<CoreApp>} [props={}] - UserApp properties
|
|
@@ -19,16 +22,35 @@ class UserApp extends CoreApp {
|
|
|
19
22
|
super(props)
|
|
20
23
|
this.registerCommand("setUser", this.setUser.bind(this))
|
|
21
24
|
this.registerCommand("welcome", this.welcome.bind(this))
|
|
25
|
+
this.registerCommand("deps", this.handleDeps.bind(this)) // Register new command
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Handle deps command with async generator for stream processing.
|
|
30
|
+
* @param {DepsCommand} cmd - Command message with deps parameters
|
|
31
|
+
* @param {UserUI} ui - UI instance
|
|
32
|
+
* @returns {Promise<Object>} Command output
|
|
33
|
+
*/
|
|
34
|
+
async handleDeps(cmd, ui) {
|
|
35
|
+
// Example: Use async generator to stream deps processing
|
|
36
|
+
const processorFn = async () => new UIStream.StreamEntry({ value: { message: `Deps command executed with fix: ${cmd.body.fix}` }, done: true })
|
|
37
|
+
const generatorFn = UIStream.createProcessor(new AbortController().signal, processorFn)
|
|
38
|
+
await UIStream.process(new AbortController().signal, generatorFn,
|
|
39
|
+
(progress, item) => ui.output && ui.output(item.value), // Fix to output the value
|
|
40
|
+
(error) => ui.output && ui.output({ error }), // Assume ui has output method
|
|
41
|
+
(item) => ui.output && ui.output(item.value) // Fix complete callback
|
|
42
|
+
)
|
|
43
|
+
return { completed: true }
|
|
22
44
|
}
|
|
23
45
|
|
|
24
46
|
/**
|
|
25
47
|
* Set user data from params.
|
|
26
|
-
* @param {
|
|
48
|
+
* @param {UserAppCommandMessage} cmd - Command message with user data
|
|
27
49
|
* @param {UserUI} ui - UI instance
|
|
28
50
|
* @returns {Promise<{ message: string }>} Welcome message
|
|
29
51
|
*/
|
|
30
52
|
async setUser(cmd, ui) {
|
|
31
|
-
this.state.user = User.from(cmd.
|
|
53
|
+
this.state.user = User.from(cmd.body.user) // cmd is UserAppCommandMessage, has user
|
|
32
54
|
const frame = await this.welcome(cmd, ui)
|
|
33
55
|
return {
|
|
34
56
|
message: String(frame)
|
|
@@ -37,22 +59,20 @@ class UserApp extends CoreApp {
|
|
|
37
59
|
|
|
38
60
|
/**
|
|
39
61
|
* Show welcome message for current user.
|
|
40
|
-
* @param {
|
|
62
|
+
* @param {UserAppCommandMessage} cmd - Command message
|
|
41
63
|
* @param {UserUI} ui - UI instance
|
|
42
64
|
* @returns {Promise<string[][]>} Welcome view output
|
|
43
65
|
*/
|
|
44
66
|
async welcome(cmd, ui) {
|
|
45
|
-
if (cmd.
|
|
46
|
-
const user = User.from(cmd.
|
|
67
|
+
if (cmd.body.user) { // cmd is UserAppCommandMessage, has user
|
|
68
|
+
const user = User.from(cmd.body.user)
|
|
47
69
|
return ui.render("Welcome", { user })
|
|
48
70
|
}
|
|
49
71
|
if (notEmpty(this.user)) {
|
|
50
72
|
return ui.render("Welcome", { user: this.user })
|
|
51
73
|
}
|
|
52
|
-
const answer = await ui.ask(
|
|
74
|
+
const answer = await ui.ask(UiMessage.from("What is your name?"))
|
|
53
75
|
this.user = User.from(answer?.value)
|
|
54
76
|
return ui.render("Welcome", { user: this.user })
|
|
55
77
|
}
|
|
56
78
|
}
|
|
57
|
-
|
|
58
|
-
export default UserApp
|
package/src/README.md.js
CHANGED
|
@@ -8,17 +8,13 @@ import {
|
|
|
8
8
|
runSpawn,
|
|
9
9
|
} from "@nan0web/test"
|
|
10
10
|
import {
|
|
11
|
-
App,
|
|
12
|
-
Component,
|
|
13
11
|
Frame,
|
|
14
|
-
InputMessage,
|
|
15
12
|
Model,
|
|
16
13
|
OutputMessage,
|
|
17
|
-
UIMessage,
|
|
18
|
-
UIForm,
|
|
19
|
-
UIStream,
|
|
20
14
|
View,
|
|
21
15
|
FormInput,
|
|
16
|
+
UiMessage,
|
|
17
|
+
UiForm,
|
|
22
18
|
} from "./index.js"
|
|
23
19
|
import { Welcome } from "./Component/index.js"
|
|
24
20
|
|
|
@@ -108,8 +104,7 @@ function testRender() {
|
|
|
108
104
|
*
|
|
109
105
|
* UI communication is built around messages:
|
|
110
106
|
*
|
|
111
|
-
* - **`
|
|
112
|
-
* - **`InputMessage`** – user input message (value, options)
|
|
107
|
+
* - **`UiMessage`** – abstract message base class
|
|
113
108
|
* - **`OutputMessage`** – system output (content, error, priority)
|
|
114
109
|
*
|
|
115
110
|
* Messages are simple, serializable data containers. They help build
|
|
@@ -118,19 +113,22 @@ function testRender() {
|
|
|
118
113
|
it("How to create input and output messages?", () => {
|
|
119
114
|
//import { InputMessage, OutputMessage } from '@nan0web/ui'
|
|
120
115
|
|
|
121
|
-
const input =
|
|
116
|
+
const input = UiMessage.from({ body: 'Hello User' })
|
|
122
117
|
const output = OutputMessage.from({ content: ['Welcome to @nan0web/ui'] })
|
|
123
|
-
console.info(input
|
|
124
|
-
console.info(output
|
|
125
|
-
assert.deepStrictEqual(
|
|
126
|
-
assert.
|
|
118
|
+
console.info(input) // ← Message { body: "Hello User", head: {}, id: "....", type: "" }
|
|
119
|
+
console.info(String(output)) // ← Welcome to @nan0web/ui
|
|
120
|
+
assert.deepStrictEqual(console.output()[0][1].body, "Hello User")
|
|
121
|
+
assert.deepStrictEqual(console.output()[0][1].head, {})
|
|
122
|
+
assert.deepStrictEqual(console.output()[0][1].type, "")
|
|
123
|
+
assert.ok(console.output()[0][1].id)
|
|
124
|
+
assert.ok(console.output()[1][1].endsWith('Welcome to @nan0web/ui'))
|
|
127
125
|
})
|
|
128
126
|
|
|
129
127
|
/**
|
|
130
128
|
* @docs
|
|
131
129
|
* ### Forms
|
|
132
130
|
*
|
|
133
|
-
* `
|
|
131
|
+
* `UiForm` supports field definitions, data management, and schema validation.
|
|
134
132
|
* Every form includes a title, fields, and current state.
|
|
135
133
|
*
|
|
136
134
|
* Field types include:
|
|
@@ -142,10 +140,10 @@ function testRender() {
|
|
|
142
140
|
* - `checkbox`
|
|
143
141
|
* - `textarea`
|
|
144
142
|
*/
|
|
145
|
-
it("How to define and validate a
|
|
146
|
-
//import {
|
|
143
|
+
it("How to define and validate a UiForm?", () => {
|
|
144
|
+
//import { UiForm } from '@nan0web/ui'
|
|
147
145
|
|
|
148
|
-
const form = new
|
|
146
|
+
const form = new UiForm({
|
|
149
147
|
title: "Contact Form",
|
|
150
148
|
fields: [
|
|
151
149
|
FormInput.from({ name: "email", label: "Email Address", type: "email", required: true }),
|
|
@@ -157,12 +155,12 @@ function testRender() {
|
|
|
157
155
|
}
|
|
158
156
|
})
|
|
159
157
|
|
|
160
|
-
const
|
|
161
|
-
console.info(
|
|
162
|
-
console.info(
|
|
158
|
+
const errors = form.validate()
|
|
159
|
+
console.info(errors.size) // ← 1
|
|
160
|
+
console.info(errors.get("email")) // ← Invalid email format
|
|
163
161
|
|
|
164
|
-
assert.equal(
|
|
165
|
-
assert.equal(
|
|
162
|
+
assert.equal(console.output()[0][1], 1)
|
|
163
|
+
assert.equal(console.output()[1][1], "Invalid email format")
|
|
166
164
|
})
|
|
167
165
|
|
|
168
166
|
/**
|
|
@@ -277,13 +275,15 @@ function testRender() {
|
|
|
277
275
|
* with minimal setup.
|
|
278
276
|
*/
|
|
279
277
|
it("How to test UI components with assertions?", () => {
|
|
280
|
-
//import { Welcome
|
|
278
|
+
//import { Welcome } from '@nan0web/ui'
|
|
281
279
|
|
|
282
280
|
const output = Welcome({ user: { name: "Test" } })
|
|
283
|
-
|
|
284
|
-
console.
|
|
285
|
-
|
|
286
|
-
|
|
281
|
+
console.info(output) // ← Welcome Test!
|
|
282
|
+
assert.deepStrictEqual(console.output()[0][1], [
|
|
283
|
+
["Welcome", " ", "Test", "!"],
|
|
284
|
+
["What can we do today great?"],
|
|
285
|
+
[""],
|
|
286
|
+
])
|
|
287
287
|
})
|
|
288
288
|
|
|
289
289
|
/**
|
|
@@ -292,10 +292,10 @@ function testRender() {
|
|
|
292
292
|
*
|
|
293
293
|
* The library includes rich playground demos:
|
|
294
294
|
*
|
|
295
|
-
* - [Registration Form](./
|
|
296
|
-
* - [Currency Exchange](./
|
|
297
|
-
* - [Mobile Top-up](./
|
|
298
|
-
* - [Language Selector](./
|
|
295
|
+
* - [Registration Form](./play/registration.form.js)
|
|
296
|
+
* - [Currency Exchange](./play/currency.exchange.js)
|
|
297
|
+
* - [Mobile Top-up](./play/topup.telephone.js)
|
|
298
|
+
* - [Language Selector](./play/language.form.js)
|
|
299
299
|
*
|
|
300
300
|
* Run to explore live functionality:
|
|
301
301
|
*/
|
|
@@ -306,10 +306,10 @@ function testRender() {
|
|
|
306
306
|
* git clone https://github.com/nan0web/ui.git
|
|
307
307
|
* cd ui
|
|
308
308
|
* npm install
|
|
309
|
-
* npm run
|
|
309
|
+
* npm run play
|
|
310
310
|
* ```
|
|
311
311
|
*/
|
|
312
|
-
assert.ok(String(pkg.scripts?.
|
|
312
|
+
assert.ok(String(pkg.scripts?.play).includes("node play"))
|
|
313
313
|
const response = await runSpawn("git", ["remote", "get-url", "origin"])
|
|
314
314
|
assert.ok(response.code === 0, "git command fails (e.g., not in a git repo)")
|
|
315
315
|
assert.ok(response.text.trim().endsWith(":nan0web/ui.git"))
|
package/src/StdIn.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import EventProcessor from "@nan0web/event/oop"
|
|
2
2
|
import { typeOf } from "@nan0web/types"
|
|
3
|
-
import
|
|
3
|
+
import { UiMessage } from "./core/index.js"
|
|
4
4
|
|
|
5
5
|
class Processor extends EventProcessor { }
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Handles standard input stream with message buffering.
|
|
9
9
|
*/
|
|
10
|
-
class StdIn extends EventProcessor {
|
|
10
|
+
export default class StdIn extends EventProcessor {
|
|
11
11
|
/** @type {number} Read interval in milliseconds */
|
|
12
12
|
static READ_INTERVAL = 99
|
|
13
13
|
|
|
14
14
|
/** @type {string[]} Messages to ignore */
|
|
15
15
|
static IGNORE_MESSAGES = ["", "undefined"]
|
|
16
16
|
|
|
17
|
-
/** @type {
|
|
17
|
+
/** @type {UiMessage[]} Input message buffer */
|
|
18
18
|
stream = []
|
|
19
19
|
|
|
20
20
|
/** @type {Processor} Input processor */
|
|
@@ -24,7 +24,7 @@ class StdIn extends EventProcessor {
|
|
|
24
24
|
* Creates a new StdIn instance.
|
|
25
25
|
* @param {object} props - StdIn properties
|
|
26
26
|
* @param {Processor} [props.processor] - Input processor
|
|
27
|
-
* @param {
|
|
27
|
+
* @param {UiMessage[]} [props.stream=[]] - Initial input stream
|
|
28
28
|
*/
|
|
29
29
|
constructor(props = {}) {
|
|
30
30
|
super()
|
|
@@ -58,13 +58,13 @@ class StdIn extends EventProcessor {
|
|
|
58
58
|
/**
|
|
59
59
|
* Reads a message from the input stream.
|
|
60
60
|
* Waits until messages are available if stream is empty.
|
|
61
|
-
* @returns {Promise<
|
|
61
|
+
* @returns {Promise<UiMessage>} Next input message
|
|
62
62
|
*/
|
|
63
63
|
async read() {
|
|
64
64
|
while (this.ended) {
|
|
65
65
|
await new Promise(resolve => setTimeout(resolve, StdIn.READ_INTERVAL))
|
|
66
66
|
}
|
|
67
|
-
return this.stream.shift() ?? new
|
|
67
|
+
return this.stream.shift() ?? new UiMessage()
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
/**
|
|
@@ -83,16 +83,16 @@ class StdIn extends EventProcessor {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
/**
|
|
86
|
-
* Decodes a message into an
|
|
87
|
-
* @param {
|
|
88
|
-
* @returns {
|
|
86
|
+
* Decodes a message into an UiMessage instance.
|
|
87
|
+
* @param {UiMessage | string[] | any} message - Message to decode
|
|
88
|
+
* @returns {UiMessage} Decoded input message
|
|
89
89
|
*/
|
|
90
90
|
decode(message) {
|
|
91
|
-
if (message instanceof
|
|
91
|
+
if (message instanceof UiMessage) return message
|
|
92
92
|
if (Array.isArray(message) && message.every(typeOf(String))) {
|
|
93
|
-
return new
|
|
93
|
+
return new UiMessage({ value: message })
|
|
94
94
|
}
|
|
95
|
-
return new
|
|
95
|
+
return new UiMessage(message)
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
/**
|
|
@@ -106,4 +106,3 @@ class StdIn extends EventProcessor {
|
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
export default StdIn
|
package/src/View/View.js
CHANGED
|
@@ -3,13 +3,13 @@ import Frame, { FrameRenderMethod } from "../Frame/Frame.js"
|
|
|
3
3
|
import Locale from "../Locale.js"
|
|
4
4
|
import StdOut from "../StdOut.js"
|
|
5
5
|
import StdIn from "../StdIn.js"
|
|
6
|
-
import InputMessage from "../core/Message/InputMessage.js"
|
|
7
6
|
import RenderOptions from "./RenderOptions.js"
|
|
7
|
+
import UiMessage from "../core/Message/Message.js"
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* @typedef {Object} ComponentFn
|
|
11
11
|
* @property {string} name
|
|
12
|
-
* @property {(input:
|
|
12
|
+
* @property {(input: UiMessage) => Promise<any>} ask
|
|
13
13
|
* @property {Function} bind
|
|
14
14
|
*/
|
|
15
15
|
|
|
@@ -256,8 +256,8 @@ export default class View {
|
|
|
256
256
|
}
|
|
257
257
|
|
|
258
258
|
/**
|
|
259
|
-
* @param {
|
|
260
|
-
* @returns {Promise<
|
|
259
|
+
* @param {UiMessage} input
|
|
260
|
+
* @returns {Promise<UiMessage | null>}
|
|
261
261
|
*/
|
|
262
262
|
async ask(input) {
|
|
263
263
|
const name = input.constructor.name.replace(/Input$/, "")
|
|
@@ -268,7 +268,7 @@ export default class View {
|
|
|
268
268
|
let result = null
|
|
269
269
|
do {
|
|
270
270
|
const answer = await this.stdin.read()
|
|
271
|
-
result = /** @type {typeof
|
|
271
|
+
result = /** @type {typeof UiMessage} */ (input.constructor).from(answer)
|
|
272
272
|
} while (!result.isValid && !result.escaped)
|
|
273
273
|
return result.escaped ? null : result
|
|
274
274
|
}
|