@nan0web/ui 1.0.1 → 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.
Files changed (46) hide show
  1. package/README.md +8 -35
  2. package/package.json +7 -2
  3. package/src/App/Core/CoreApp.js +14 -19
  4. package/src/App/Core/UI.js +5 -5
  5. package/src/App/User/UserApp.js +4 -19
  6. package/src/App/User/UserUI.js +4 -4
  7. package/src/App/User/index.js +0 -6
  8. package/src/App/index.js +0 -3
  9. package/src/README.md.js +22 -23
  10. package/src/StdIn.js +1 -3
  11. package/src/core/Error/CancelError.js +6 -0
  12. package/src/core/Error/index.js +9 -0
  13. package/src/core/Form/Form.js +36 -18
  14. package/src/core/Form/Input.js +16 -7
  15. package/src/core/Form/Message.js +6 -9
  16. package/src/core/InputAdapter.js +25 -3
  17. package/src/core/Message/InputMessage.js +11 -11
  18. package/src/core/Message/OutputMessage.js +1 -1
  19. package/src/core/index.js +2 -0
  20. package/src/index.js +1 -0
  21. package/types/App/Core/CoreApp.d.ts +9 -9
  22. package/types/App/Core/UI.d.ts +5 -5
  23. package/types/App/Core/Widget.d.ts +1 -1
  24. package/types/App/User/Command/Message.d.ts +2 -3
  25. package/types/App/User/Command/Options.d.ts +9 -2
  26. package/types/App/User/Command/index.d.ts +1 -4
  27. package/types/App/User/UserApp.d.ts +10 -7
  28. package/types/App/User/UserUI.d.ts +0 -9
  29. package/types/App/User/index.d.ts +0 -4
  30. package/types/App/index.d.ts +1 -3
  31. package/types/Frame/Frame.d.ts +5 -5
  32. package/types/View/View.d.ts +3 -3
  33. package/types/core/Error/CancelError.d.ts +3 -0
  34. package/types/core/Error/index.d.ts +6 -0
  35. package/types/core/Form/Form.d.ts +12 -6
  36. package/types/core/Form/Input.d.ts +20 -7
  37. package/types/core/Form/Message.d.ts +10 -4
  38. package/types/core/InputAdapter.d.ts +20 -2
  39. package/types/core/Message/InputMessage.d.ts +5 -5
  40. package/types/core/Message/OutputMessage.d.ts +1 -1
  41. package/types/core/Stream.d.ts +1 -1
  42. package/types/core/StreamEntry.d.ts +1 -1
  43. package/types/core/index.d.ts +1 -0
  44. package/types/index.d.ts +1 -0
  45. package/src/App/Command/Options.js +0 -78
  46. package/src/App/Command/index.js +0 -9
package/README.md CHANGED
@@ -1,8 +1,6 @@
1
1
  # @nan0web/ui
2
2
 
3
- |[Status](https://github.com/nan0web/monorepo/blob/main/system.md#написання-сценаріїв)|Documentation|Test coverage|Features|Npm version|
4
- |---|---|---|---|---|
5
- |🟢 `96.6%` |🧪 [English 🏴󠁧󠁢󠁥󠁮󠁧󠁿](https://github.com/nan0web/ui/blob/main/README.md)<br />[Українською 🇺🇦](https://github.com/nan0web/ui/blob/main/docs/uk/README.md) |🟡 `80.9%` |✅ d.ts 📜 system.md 🕹️ playground |— |
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"
@@ -132,41 +124,24 @@ console.info(String(view.frame)) // ← "\rHello, world"
132
124
  `Frame` manages visual rendering with width and height limits.
133
125
  Useful for fixed-size terminals or UI blocks.
134
126
 
127
+ Render methods:
128
+
129
+ - `APPEND` – adds content after previous frame
130
+ - `REPLACE` – erases and replaces full frame area
131
+ - `VISIBLE` – renders only visible part of frame
132
+
135
133
  How to create a Frame with fixed size?
136
134
  ```js
137
135
  import { Frame } from '@nan0web/ui'
138
-
139
136
  const frame = new Frame({
140
137
  value: [["Frame content"]],
141
138
  width: 20,
142
139
  height: 5,
143
140
  renderMethod: Frame.RenderMethod.APPEND,
144
141
  })
145
-
146
142
  const rendered = frame.render()
147
143
  console.info(rendered.includes("Frame content")) // ← true
148
144
  ```
149
- ### App Architecture
150
-
151
- `App` provides the main application logic.
152
-
153
- - Core – minimal UI layer
154
- - User – user-specific UI commands
155
-
156
- Each app registers commands and binds them to UI actions.
157
-
158
- How to create a basic user app that greets?
159
- ```js
160
- import { App, View } from '@nan0web/ui'
161
-
162
- const app = new App.User.App({ name: "GreetApp" })
163
- const view = new View()
164
- view.register("Welcome", Welcome)
165
-
166
- const cmd = App.Command.Message.parse("welcome --user Bob")
167
- const result = await app.processCommand(cmd, new App.User.UI(app, view))
168
- console.info(String(result)) // ← Welcome Bob!
169
- ```
170
145
  ### Models
171
146
 
172
147
  UI models are plain data objects managed by `Model` classes.
@@ -176,7 +151,6 @@ UI models are plain data objects managed by `Model` classes.
176
151
  How to use a User model?
177
152
  ```js
178
153
  import { Model } from '@nan0web/ui'
179
-
180
154
  const user = new Model.User({ name: "Charlie", email: "charlie@example.com" })
181
155
  console.info(user.name) // ← Charlie
182
156
  console.info(user.email) // ← charlie@example.com
@@ -191,7 +165,6 @@ with minimal setup.
191
165
  How to test UI components with assertions?
192
166
  ```js
193
167
  import { Welcome, InputMessage } from '@nan0web/ui'
194
-
195
168
  const output = Welcome({ user: { name: "Test" } })
196
169
  const input = InputMessage.from({ value: "test" })
197
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.1",
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\" | nan0test parse --fail",
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"
@@ -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 {CommandMessage} Starting command parsed from argv */
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 {string[]} [props.argv=[]] - Command line arguments to parse
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
- argv = [],
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
- this.startCommand = CommandMessage.parse(argv)
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 {CommandMessage} commandMessage - Command to process
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(commandMessage, ui) {
87
- const cmd = commandMessage.args[0]
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
- cmd, "\n",
92
+ msg.constructor.name, "\n",
93
93
  "Available commands", ": ",
94
94
  [...this.commands.keys()].join(", "),
95
95
  ].join(""))
96
96
  }
97
- const Class = /** @type {typeof CommandMessage} */ (commandMessage.constructor)
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 {CommandMessage[]} commandMessages - Array of commands to process
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(commandMessages, ui) {
106
+ async processCommands(Messages, ui) {
112
107
  const results = []
113
- for (const cmdMsg of commandMessages) {
108
+ for (const cmdMsg of Messages) {
114
109
  const result = await this.processCommand(cmdMsg, ui)
115
110
  results.push(result)
116
111
  }
@@ -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 CommandMessage array.
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 {CommandMessage[]} Array of command messages
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 CommandMessage({
61
+ const cmd = new Message({
62
62
  argv: [answer],
63
63
  opts: input?.opts ?? {},
64
64
  })
@@ -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 {object} props - UserApp properties
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 {CommandMessage} cmd - Command message with user data
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 {CommandMessage} cmd - Command message
40
+ * @param {Message} cmd - Command message
56
41
  * @param {UserUI} ui - UI instance
57
42
  * @returns {Promise<string[][]>} Welcome view output
58
43
  */
@@ -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 CommandMessage array.
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 {CommandMessage[]} Array of command messages
15
+ * @returns {Message[]} Array of command messages
16
16
  */
17
17
  convertInput(rawInput) {
18
- return [CommandMessage.parse(rawInput)]
18
+ return [new Message({ body: rawInput })]
19
19
  }
20
20
  }
@@ -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.equal(input.value, 'Hello User')
126
- assert.equal(output.content[0], 'Welcome to @nan0web/ui')
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
  /**
@@ -210,6 +210,12 @@ function testRender() {
210
210
  *
211
211
  * `Frame` manages visual rendering with width and height limits.
212
212
  * Useful for fixed-size terminals or UI blocks.
213
+ *
214
+ * Render methods:
215
+ *
216
+ * - `APPEND` – adds content after previous frame
217
+ * - `REPLACE` – erases and replaces full frame area
218
+ * - `VISIBLE` – renders only visible part of frame
213
219
  */
214
220
  it("How to create a Frame with fixed size?", () => {
215
221
  //import { Frame } from '@nan0web/ui'
@@ -225,29 +231,22 @@ function testRender() {
225
231
  console.info(rendered.includes("Frame content")) // ← true
226
232
  assert.ok(rendered.includes("Frame content"))
227
233
  })
234
+ it("How to create a Frame with different render methods?", () => {
235
+ //import { Frame } from '@nan0web/ui'
228
236
 
229
- /**
230
- * @docs
231
- * ### App Architecture
232
- *
233
- * `App` provides the main application logic.
234
- *
235
- * - Core – minimal UI layer
236
- * - User – user-specific UI commands
237
- *
238
- * Each app registers commands and binds them to UI actions.
239
- */
240
- it("How to create a basic user app that greets?", async () => {
241
- //import { App, View } from '@nan0web/ui'
237
+ const frame = new Frame({
238
+ value: [["Frame content"]],
239
+ width: 20,
240
+ height: 5,
241
+ })
242
242
 
243
- const app = new App.User.App({ name: "GreetApp" })
244
- const view = new View()
245
- view.register("Welcome", Welcome)
243
+ frame.renderMethod = Frame.RenderMethod.REPLACE
244
+ const renderedReplace = frame.render()
245
+ assert.ok(renderedReplace.includes("Frame content"))
246
246
 
247
- const cmd = App.Command.Message.parse("welcome --user Bob")
248
- const result = await app.processCommand(cmd, new App.User.UI(app, view))
249
- console.info(String(result)) // ← Welcome Bob!
250
- assert.ok(console.output()[0][1].includes("Welcome Bob!"))
247
+ frame.renderMethod = Frame.RenderMethod.VISIBLE
248
+ const renderedVisible = frame.render()
249
+ assert.ok(renderedVisible.includes("Frame content"))
251
250
  })
252
251
 
253
252
  /**
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
- const parsed = CommandMessage.parse(message)
95
- return new InputMessage({ value: parsed })
93
+ return new InputMessage({ value: message })
96
94
  }
97
95
  return new InputMessage(message)
98
96
  }
@@ -0,0 +1,6 @@
1
+ export default class CancelError extends Error {
2
+ constructor(message = "Operation cancelled by user") {
3
+ super(message)
4
+ this.name = "CancelError"
5
+ }
6
+ }
@@ -0,0 +1,9 @@
1
+ import CancelError from "./CancelError.js"
2
+
3
+ export {
4
+ CancelError
5
+ }
6
+
7
+ export default {
8
+ CancelError
9
+ }
@@ -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 validator registry */
22
+ /* static validation registry */
22
23
  /* ------------------------------------------------------------------ */
23
24
 
24
25
  /** @type {Object<string,Function>} */
25
- static _validators = {}
26
+ static _validations = {}
26
27
 
27
28
  /**
28
- * Register a custom validator that can be referenced by name in a schema.
29
+ * Register a custom validation that can be referenced by name in a schema.
29
30
  *
30
- * @param {string} name - Identifier used in schema.validator.
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 addValidator(name, fn) {
35
+ static addValidation(name, fn) {
35
36
  if (typeof name !== "string" || typeof fn !== "function") {
36
- throw new Error("Validator name must be a string and fn must be a function")
37
+ throw new Error("validation name must be a string and fn must be a function")
37
38
  }
38
- UIForm._validators[name] = fn
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 validator – can be a function or a string referencing a static validator
212
- if (schema.validator) {
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.validator === 'function') {
215
- result = schema.validator(value)
216
- } else if (typeof schema.validator === 'string') {
217
- const fn = UIForm._validators[schema.validator]
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(`Validator "${schema.validator}" not registered`)
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
- id: this.id,
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
- validator: custom.validator ?? undefined,
301
+ validation: custom.validation ?? undefined,
284
302
  defaultValue: custom.defaultValue ?? "",
285
303
  })
286
304
  })
@@ -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} validator - Custom validation function.
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 {Array<string>} */ options = []
21
- /** @type {Function|null} */ validator = 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 {Array<string>} [props.options=[]] - Select options.
46
- * @param {Function} [props.validator=null] - Custom validator.
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
- validator = this.validator,
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.validator = validator
80
+ this.validation = validation
72
81
  this.defaultValue = defaultValue
73
82
 
74
83
  this.requireValidType()