@nan0web/ui 1.0.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/.datasets/README.dataset.jsonl +12 -0
- package/.editorconfig +20 -0
- package/CONTRIBUTING.md +42 -0
- package/LICENSE +15 -0
- package/README.md +238 -0
- package/docs/uk/README.md +240 -0
- package/package.json +64 -0
- package/playground/User.js +52 -0
- package/playground/currency.exchange.js +48 -0
- package/playground/i18n/index.js +21 -0
- package/playground/i18n/uk.js +53 -0
- package/playground/language.form.js +25 -0
- package/playground/main.js +72 -0
- package/playground/registration.form.js +58 -0
- package/playground/topup.telephone.js +62 -0
- package/src/App/Command/Options.js +78 -0
- package/src/App/Command/index.js +9 -0
- package/src/App/Core/CoreApp.js +129 -0
- package/src/App/Core/UI.js +116 -0
- package/src/App/Core/Widget.js +67 -0
- package/src/App/Core/index.js +11 -0
- package/src/App/Scenario.js +45 -0
- package/src/App/User/Command/Message.js +44 -0
- package/src/App/User/Command/Options.js +48 -0
- package/src/App/User/Command/index.js +11 -0
- package/src/App/User/UserApp.js +73 -0
- package/src/App/User/UserApp.test.js +56 -0
- package/src/App/User/UserUI.js +20 -0
- package/src/App/User/UserUI.test.js +51 -0
- package/src/App/User/index.js +15 -0
- package/src/App/index.js +22 -0
- package/src/Component/Process/Input.js +70 -0
- package/src/Component/Process/Process.js +26 -0
- package/src/Component/Process/index.js +5 -0
- package/src/Component/Welcome/Input.js +50 -0
- package/src/Component/Welcome/Welcome.js +26 -0
- package/src/Component/Welcome/index.js +5 -0
- package/src/Component/index.js +9 -0
- package/src/Frame/Frame.js +591 -0
- package/src/Frame/Frame.test.js +429 -0
- package/src/Frame/Props.js +102 -0
- package/src/Locale.js +119 -0
- package/src/Model/User/User.js +56 -0
- package/src/Model/index.js +7 -0
- package/src/README.md.js +371 -0
- package/src/StdIn.js +111 -0
- package/src/StdOut.js +99 -0
- package/src/View/RenderOptions.js +48 -0
- package/src/View/View.js +289 -0
- package/src/View/View.test.js +77 -0
- package/src/core/Form/Form.js +289 -0
- package/src/core/Form/Form.test.js +116 -0
- package/src/core/Form/Input.js +116 -0
- package/src/core/Form/Input.test.js +58 -0
- package/src/core/Form/Message.js +86 -0
- package/src/core/Form/Message.test.js +54 -0
- package/src/core/Form/index.js +11 -0
- package/src/core/InputAdapter.js +41 -0
- package/src/core/InputAdapter.test.js +35 -0
- package/src/core/Message/InputMessage.js +119 -0
- package/src/core/Message/InputMessage.test.js +45 -0
- package/src/core/Message/Message.js +77 -0
- package/src/core/Message/Message.test.js +58 -0
- package/src/core/Message/OutputMessage.js +143 -0
- package/src/core/Message/OutputMessage.test.js +61 -0
- package/src/core/Message/index.js +7 -0
- package/src/core/OutputAdapter.js +50 -0
- package/src/core/OutputAdapter.test.js +35 -0
- package/src/core/Stream.js +71 -0
- package/src/core/Stream.test.js +78 -0
- package/src/core/StreamEntry.js +59 -0
- package/src/core/index.js +13 -0
- package/src/functions.js +38 -0
- package/src/index.js +34 -0
- package/src/index.test.js +14 -0
- package/src/models/SimpleUser.js +18 -0
- package/stories/App/AppView.js +15 -0
- package/stories/App/AppView.test.js +22 -0
- package/stories/App/RenderOptions.js +14 -0
- package/stories/nodejs/interface.test.js +27 -0
- package/system.md +187 -0
- package/system1.md +137 -0
- package/task.md +181 -0
- package/tsconfig.json +23 -0
- package/types/App/Command/Options.d.ts +46 -0
- package/types/App/Command/index.d.ts +8 -0
- package/types/App/Core/CoreApp.d.ts +70 -0
- package/types/App/Core/UI.d.ts +49 -0
- package/types/App/Core/Widget.d.ts +40 -0
- package/types/App/Core/index.d.ts +10 -0
- package/types/App/Scenario.d.ts +26 -0
- package/types/App/User/Command/Message.d.ts +30 -0
- package/types/App/User/Command/Options.d.ts +27 -0
- package/types/App/User/Command/index.d.ts +8 -0
- package/types/App/User/UserApp.d.ts +31 -0
- package/types/App/User/UserUI.d.ts +18 -0
- package/types/App/User/index.d.ts +12 -0
- package/types/App/index.d.ts +14 -0
- package/types/Component/Process/Input.d.ts +48 -0
- package/types/Component/Process/Process.d.ts +13 -0
- package/types/Component/Process/index.d.ts +4 -0
- package/types/Component/Welcome/Input.d.ts +34 -0
- package/types/Component/Welcome/Welcome.d.ts +13 -0
- package/types/Component/Welcome/index.d.ts +4 -0
- package/types/Component/index.d.ts +8 -0
- package/types/Frame/Frame.d.ts +186 -0
- package/types/Frame/Props.d.ts +77 -0
- package/types/Locale.d.ts +55 -0
- package/types/Model/User/User.d.ts +36 -0
- package/types/Model/index.d.ts +6 -0
- package/types/StdIn.d.ts +62 -0
- package/types/StdOut.d.ts +52 -0
- package/types/View/RenderOptions.d.ts +29 -0
- package/types/View/View.d.ts +115 -0
- package/types/core/Form/Form.d.ts +123 -0
- package/types/core/Form/Input.d.ts +69 -0
- package/types/core/Form/Message.d.ts +28 -0
- package/types/core/Form/index.d.ts +5 -0
- package/types/core/InputAdapter.d.ts +28 -0
- package/types/core/Message/InputMessage.d.ts +71 -0
- package/types/core/Message/Message.d.ts +50 -0
- package/types/core/Message/OutputMessage.d.ts +53 -0
- package/types/core/Message/index.d.ts +5 -0
- package/types/core/OutputAdapter.d.ts +33 -0
- package/types/core/Stream.d.ts +27 -0
- package/types/core/StreamEntry.d.ts +45 -0
- package/types/core/index.d.ts +9 -0
- package/types/functions.d.ts +3 -0
- package/types/index.d.ts +20 -0
- package/types/models/SimpleUser.d.ts +21 -0
- package/vitest.config.js +26 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { describe, it } from "node:test"
|
|
2
|
+
import { strict as assert } from "node:assert"
|
|
3
|
+
import UIForm from "./Form.js"
|
|
4
|
+
import FormInput from "./Input.js"
|
|
5
|
+
|
|
6
|
+
describe("UIForm", () => {
|
|
7
|
+
it("should create instance with default values", () => {
|
|
8
|
+
const form = new UIForm()
|
|
9
|
+
assert.ok(form instanceof UIForm)
|
|
10
|
+
assert.equal(form.title, "")
|
|
11
|
+
assert.deepEqual(form.fields, [])
|
|
12
|
+
assert.deepEqual(form.state, {})
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it("should create instance with custom values", () => {
|
|
16
|
+
const fields = [
|
|
17
|
+
new FormInput({ name: "name", label: "Name", required: true }),
|
|
18
|
+
new FormInput({ name: "email", label: "Email", type: "email" })
|
|
19
|
+
]
|
|
20
|
+
const props = {
|
|
21
|
+
title: "Test Form",
|
|
22
|
+
fields,
|
|
23
|
+
state: { name: "John" }
|
|
24
|
+
}
|
|
25
|
+
const form = new UIForm(props)
|
|
26
|
+
assert.equal(form.title, "Test Form")
|
|
27
|
+
assert.equal(form.fields.length, 2)
|
|
28
|
+
assert.deepEqual(form.state, { name: "John" })
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it("should set data", () => {
|
|
32
|
+
const form = new UIForm({ state: { name: "John" } })
|
|
33
|
+
const newForm = form.setData({ email: "john@example.com" })
|
|
34
|
+
assert.deepEqual(newForm.state, { name: "John", email: "john@example.com" })
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it("should get field by name", () => {
|
|
38
|
+
const fields = [new FormInput({ name: "test", label: "Test Field" })]
|
|
39
|
+
const form = new UIForm({ fields })
|
|
40
|
+
const field = form.getField("test")
|
|
41
|
+
assert.ok(field)
|
|
42
|
+
assert.equal(field.name, "test")
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it("should get values", () => {
|
|
46
|
+
const form = new UIForm({ state: { name: "John", age: 30 } })
|
|
47
|
+
const values = form.getValues()
|
|
48
|
+
assert.deepEqual(values, { name: "John", age: 30 })
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it("should validate form", () => {
|
|
52
|
+
const fields = [new FormInput({ name: "requiredField", required: true })]
|
|
53
|
+
const form = new UIForm({ fields, state: {} })
|
|
54
|
+
const result = form.validate()
|
|
55
|
+
assert.ok(!result.isValid)
|
|
56
|
+
assert.ok(result.errors.requiredField)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it("should validate individual field", () => {
|
|
60
|
+
const fields = [new FormInput({ name: "email", type: "email" })]
|
|
61
|
+
const form = new UIForm({ fields })
|
|
62
|
+
const result = form.validateField("email", "invalid-email")
|
|
63
|
+
assert.ok(!result.isValid)
|
|
64
|
+
assert.ok(result.errors.email)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it("should convert to JSON", () => {
|
|
68
|
+
const form = new UIForm({ title: "Test", state: { name: "John" } })
|
|
69
|
+
const json = form.toJSON()
|
|
70
|
+
assert.ok(json.id)
|
|
71
|
+
assert.equal(json.title, "Test")
|
|
72
|
+
assert.ok(Array.isArray(json.fields))
|
|
73
|
+
assert.ok(json.state)
|
|
74
|
+
assert.ok(json.meta)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it("parses default text fields", () => {
|
|
78
|
+
const data = { name: "", email: "" }
|
|
79
|
+
const { fields } = UIForm.parse(data)
|
|
80
|
+
assert.equal(fields.length, 2)
|
|
81
|
+
assert.ok(fields[0] instanceof FormInput)
|
|
82
|
+
assert.equal(fields[0].name, "name")
|
|
83
|
+
assert.equal(fields[0].label, "Name")
|
|
84
|
+
assert.equal(fields[0].type, FormInput.TYPES.TEXT)
|
|
85
|
+
assert.equal(fields[0].required, false)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it("applies overrides correctly", () => {
|
|
89
|
+
const data = { age: "" }
|
|
90
|
+
const { fields } = UIForm.parse(data, {
|
|
91
|
+
age: { type: FormInput.TYPES.NUMBER, required: true, label: "User Age" },
|
|
92
|
+
})
|
|
93
|
+
const ageField = fields[0]
|
|
94
|
+
assert.equal(ageField.type, FormInput.TYPES.NUMBER)
|
|
95
|
+
assert.ok(ageField.required)
|
|
96
|
+
assert.equal(ageField.label, "User Age")
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it("supports static custom validators", () => {
|
|
100
|
+
// register a validator that ensures a string contains only digits
|
|
101
|
+
UIForm.addValidator("digitsOnly", value => {
|
|
102
|
+
return /^\d+$/.test(value) ? true : "Must contain only digits"
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
const fields = [
|
|
106
|
+
new FormInput({ name: "phone", label: "Phone", required: true })
|
|
107
|
+
]
|
|
108
|
+
const schema = {
|
|
109
|
+
phone: { validator: "digitsOnly" }
|
|
110
|
+
}
|
|
111
|
+
const form = new UIForm({ fields, schema, state: { phone: "12a34" } })
|
|
112
|
+
const result = form.validate()
|
|
113
|
+
assert.ok(!result.isValid)
|
|
114
|
+
assert.equal(result.errors.phone, "Must contain only digits")
|
|
115
|
+
})
|
|
116
|
+
})
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Form input field descriptor.
|
|
3
|
+
*
|
|
4
|
+
* @class FormInput
|
|
5
|
+
* @property {string} name - Field name.
|
|
6
|
+
* @property {string} label - Display label.
|
|
7
|
+
* @property {string} type - Input type (text, email, number, select, etc.).
|
|
8
|
+
* @property {boolean} required - Whether the field is required.
|
|
9
|
+
* @property {string} placeholder - Placeholder text.
|
|
10
|
+
* @property {Array<string>} options - Select options (if type is 'select').
|
|
11
|
+
* @property {Function|null} validator - Custom validation function.
|
|
12
|
+
* @property {*} defaultValue - Default value.
|
|
13
|
+
*/
|
|
14
|
+
export default class FormInput {
|
|
15
|
+
/** @type {string} */ name = ''
|
|
16
|
+
/** @type {string} */ label = ''
|
|
17
|
+
/** @type {string} */ type = 'text'
|
|
18
|
+
/** @type {boolean} */ required = false
|
|
19
|
+
/** @type {string} */ placeholder = ''
|
|
20
|
+
/** @type {Array<string>} */ options = []
|
|
21
|
+
/** @type {Function|null} */ validator = null
|
|
22
|
+
/** @type {*} */ defaultValue = null
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Predefined input types.
|
|
26
|
+
*/
|
|
27
|
+
static TYPES = {
|
|
28
|
+
TEXT: 'text',
|
|
29
|
+
EMAIL: 'email',
|
|
30
|
+
NUMBER: 'number',
|
|
31
|
+
SELECT: 'select',
|
|
32
|
+
CHECKBOX: 'checkbox',
|
|
33
|
+
TEXTAREA: 'textarea'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Create a new form input.
|
|
38
|
+
*
|
|
39
|
+
* @param {Object} props - Input properties.
|
|
40
|
+
* @param {string} props.name - Field name.
|
|
41
|
+
* @param {string} [props.label=props.name] - Display label.
|
|
42
|
+
* @param {string} [props.type='text'] - Input type.
|
|
43
|
+
* @param {boolean} [props.required=false] - Is required.
|
|
44
|
+
* @param {string} [props.placeholder=''] - Placeholder.
|
|
45
|
+
* @param {Array<string>} [props.options=[]] - Select options.
|
|
46
|
+
* @param {Function} [props.validator=null] - Custom validator.
|
|
47
|
+
* @param {*} [props.defaultValue=null] - Default value.
|
|
48
|
+
*/
|
|
49
|
+
constructor(props) {
|
|
50
|
+
const {
|
|
51
|
+
name,
|
|
52
|
+
label = name,
|
|
53
|
+
type = FormInput.TYPES.TEXT,
|
|
54
|
+
required = this.required,
|
|
55
|
+
placeholder = this.placeholder,
|
|
56
|
+
options = [],
|
|
57
|
+
validator = this.validator,
|
|
58
|
+
defaultValue = this.defaultValue
|
|
59
|
+
} = props
|
|
60
|
+
|
|
61
|
+
if (!name) {
|
|
62
|
+
throw new TypeError('FormInput.name is required')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this.name = String(name)
|
|
66
|
+
this.label = String(label)
|
|
67
|
+
this.type = String(type)
|
|
68
|
+
this.required = Boolean(required)
|
|
69
|
+
this.placeholder = String(placeholder)
|
|
70
|
+
this.options = options
|
|
71
|
+
this.validator = validator
|
|
72
|
+
this.defaultValue = defaultValue
|
|
73
|
+
|
|
74
|
+
this.requireValidType()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
requireValidType() {
|
|
78
|
+
if (!Object.values(FormInput.TYPES).includes(this.type)) {
|
|
79
|
+
throw new TypeError([
|
|
80
|
+
"FormInput.type is invalid!",
|
|
81
|
+
["Provided", this.type].join(": "),
|
|
82
|
+
"Available types:",
|
|
83
|
+
...Object.values(FormInput.TYPES).map(t => ` - ${t}`)
|
|
84
|
+
].join("\n"))
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Serialises the input to a plain JSON object.
|
|
90
|
+
*
|
|
91
|
+
* @returns {Object}
|
|
92
|
+
*/
|
|
93
|
+
toJSON() {
|
|
94
|
+
return {
|
|
95
|
+
name: this.name,
|
|
96
|
+
label: this.label,
|
|
97
|
+
type: this.type,
|
|
98
|
+
required: this.required,
|
|
99
|
+
placeholder: this.placeholder,
|
|
100
|
+
options: this.options,
|
|
101
|
+
defaultValue: this.defaultValue
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @param {*} input
|
|
107
|
+
* @returns {FormInput}
|
|
108
|
+
*/
|
|
109
|
+
static from(input) {
|
|
110
|
+
if (input instanceof FormInput) return input
|
|
111
|
+
if ("string" === typeof input) {
|
|
112
|
+
return new FormInput({ name: input, label: input })
|
|
113
|
+
}
|
|
114
|
+
return new FormInput(input)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, it } from "node:test"
|
|
2
|
+
import { strict as assert } from "node:assert"
|
|
3
|
+
import FormInput from "./Input.js"
|
|
4
|
+
|
|
5
|
+
describe("FormInput", () => {
|
|
6
|
+
it("should create instance with default values", () => {
|
|
7
|
+
const input = new FormInput({ name: "name" })
|
|
8
|
+
assert.ok(input instanceof FormInput)
|
|
9
|
+
assert.equal(input.type, FormInput.TYPES.TEXT)
|
|
10
|
+
assert.equal(input.required, false)
|
|
11
|
+
assert.equal(input.name, "name")
|
|
12
|
+
assert.equal(input.placeholder, "")
|
|
13
|
+
assert.deepEqual(input.options, [])
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it("should create instance with custom values", () => {
|
|
17
|
+
const props = {
|
|
18
|
+
type: "email",
|
|
19
|
+
name: "email",
|
|
20
|
+
label: "Email Address",
|
|
21
|
+
required: true,
|
|
22
|
+
placeholder: "Enter email",
|
|
23
|
+
options: ["option1", "option2"],
|
|
24
|
+
defaultValue: "test@example.com"
|
|
25
|
+
}
|
|
26
|
+
const input = new FormInput(props)
|
|
27
|
+
assert.equal(input.type, "email")
|
|
28
|
+
assert.equal(input.name, "email")
|
|
29
|
+
assert.equal(input.label, "Email Address")
|
|
30
|
+
assert.equal(input.required, true)
|
|
31
|
+
assert.equal(input.placeholder, "Enter email")
|
|
32
|
+
assert.deepEqual(input.options, ["option1", "option2"])
|
|
33
|
+
assert.equal(input.defaultValue, "test@example.com")
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it("should create from string", () => {
|
|
37
|
+
const input = FormInput.from("testField")
|
|
38
|
+
assert.equal(input.name, "testField")
|
|
39
|
+
assert.equal(input.label, "testField")
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it("should create from object", () => {
|
|
43
|
+
const objInput = { name: "test", type: "text" }
|
|
44
|
+
const input = FormInput.from(objInput)
|
|
45
|
+
assert.ok(input instanceof FormInput)
|
|
46
|
+
assert.equal(input.name, "test")
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it("should validate type", async () => {
|
|
50
|
+
const validInput = new FormInput({ name: "email", type: "email" })
|
|
51
|
+
assert.ok(validInput)
|
|
52
|
+
const fn = async () => new FormInput({ name: "email", type: "invalid" })
|
|
53
|
+
await assert.rejects(fn, {
|
|
54
|
+
name: "TypeError",
|
|
55
|
+
message: /FormInput\.type is invalid!/
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
})
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import OutputMessage from "../Message/OutputMessage.js"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* FormMessage – specialized OutputMessage for forms.
|
|
5
|
+
*
|
|
6
|
+
* @class FormMessage
|
|
7
|
+
* @extends OutputMessage
|
|
8
|
+
*/
|
|
9
|
+
export default class FormMessage extends OutputMessage {
|
|
10
|
+
/**
|
|
11
|
+
* Creates a FormMessage.
|
|
12
|
+
*
|
|
13
|
+
* @param {Object} [input={}] - Message properties.
|
|
14
|
+
*/
|
|
15
|
+
constructor(input = {}) {
|
|
16
|
+
super({
|
|
17
|
+
...input,
|
|
18
|
+
type: OutputMessage.TYPES.FORM,
|
|
19
|
+
})
|
|
20
|
+
const {
|
|
21
|
+
data = {},
|
|
22
|
+
schema = {},
|
|
23
|
+
} = input
|
|
24
|
+
|
|
25
|
+
// Store data and schema for easy access
|
|
26
|
+
this.data = data
|
|
27
|
+
this.schema = schema
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Returns a new FormMessage with merged data.
|
|
32
|
+
*
|
|
33
|
+
* @param {Object} newData - Data to merge.
|
|
34
|
+
* @returns {FormMessage}
|
|
35
|
+
*/
|
|
36
|
+
addData(newData) {
|
|
37
|
+
return new FormMessage({
|
|
38
|
+
...this,
|
|
39
|
+
data: { ...this.data, ...newData },
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Validates the provided data against the schema.
|
|
45
|
+
*
|
|
46
|
+
* @param {Object} data - Data to validate.
|
|
47
|
+
* @returns {{isValid: boolean, errors: Object}}
|
|
48
|
+
*/
|
|
49
|
+
validateData(data) {
|
|
50
|
+
const errors = {}
|
|
51
|
+
|
|
52
|
+
if (!this.schema) return { isValid: true, errors }
|
|
53
|
+
|
|
54
|
+
for (const [field, rules] of Object.entries(this.schema)) {
|
|
55
|
+
const value = data[field]
|
|
56
|
+
|
|
57
|
+
if (rules.required && (value === undefined || value === null || value === '')) {
|
|
58
|
+
errors[field] = 'Field is required'
|
|
59
|
+
continue
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (rules.type) {
|
|
63
|
+
if (rules.type === 'email') {
|
|
64
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
65
|
+
if (value && !emailRegex.test(value)) {
|
|
66
|
+
errors[field] = 'Invalid email format'
|
|
67
|
+
}
|
|
68
|
+
} else if (rules.type === 'number') {
|
|
69
|
+
if (value !== '' && isNaN(Number(value))) {
|
|
70
|
+
errors[field] = 'Must be a number'
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (rules.minLength && value && value.length < rules.minLength) {
|
|
76
|
+
errors[field] = `Minimum length is ${rules.minLength}`
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (rules.maxLength && value && value.length > rules.maxLength) {
|
|
80
|
+
errors[field] = `Maximum length is ${rules.maxLength}`
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { isValid: Object.keys(errors).length === 0, errors }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, it } from "node:test"
|
|
2
|
+
import { strict as assert } from "node:assert"
|
|
3
|
+
import FormMessage from "./Message.js"
|
|
4
|
+
|
|
5
|
+
describe("FormMessage", () => {
|
|
6
|
+
it("should create instance with default values", () => {
|
|
7
|
+
const msg = new FormMessage()
|
|
8
|
+
assert.ok(msg instanceof FormMessage)
|
|
9
|
+
assert.deepEqual(msg.data, {})
|
|
10
|
+
assert.deepEqual(msg.schema, {})
|
|
11
|
+
assert.equal(msg.type, "form")
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it("should create instance with custom values", () => {
|
|
15
|
+
const props = {
|
|
16
|
+
data: { name: "John" },
|
|
17
|
+
schema: { name: { required: true } },
|
|
18
|
+
body: ["Form content"]
|
|
19
|
+
}
|
|
20
|
+
const msg = new FormMessage(props)
|
|
21
|
+
assert.deepEqual(msg.data, { name: "John" })
|
|
22
|
+
assert.deepEqual(msg.schema, { name: { required: true } })
|
|
23
|
+
assert.deepEqual(msg.body, ["Form content"])
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it("should add data", () => {
|
|
27
|
+
const msg = new FormMessage({ data: { name: "John" } })
|
|
28
|
+
const newMsg = msg.addData({ age: 30 })
|
|
29
|
+
assert.deepEqual(newMsg.data, { name: "John", age: 30 })
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it("should validate data correctly", () => {
|
|
33
|
+
const schema = {
|
|
34
|
+
name: { required: true },
|
|
35
|
+
email: { type: "email" },
|
|
36
|
+
age: { type: "number" }
|
|
37
|
+
}
|
|
38
|
+
const msg = new FormMessage({ schema })
|
|
39
|
+
|
|
40
|
+
// Valid data
|
|
41
|
+
const validData = { name: "John", email: "john@example.com", age: "30" }
|
|
42
|
+
const validResult = msg.validateData(validData)
|
|
43
|
+
assert.ok(validResult.isValid)
|
|
44
|
+
assert.deepEqual(validResult.errors, {})
|
|
45
|
+
|
|
46
|
+
// Invalid data
|
|
47
|
+
const invalidData = { email: "invalid-email", age: "not-a-number" }
|
|
48
|
+
const invalidResult = msg.validateData(invalidData)
|
|
49
|
+
assert.ok(!invalidResult.isValid)
|
|
50
|
+
assert.ok(invalidResult.errors.name)
|
|
51
|
+
assert.ok(invalidResult.errors.email)
|
|
52
|
+
assert.ok(invalidResult.errors.age)
|
|
53
|
+
})
|
|
54
|
+
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import Event from "@nan0web/event/oop"
|
|
2
|
+
import InputMessage from "./Message/InputMessage.js"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Abstract input adapter for UI implementations.
|
|
6
|
+
*
|
|
7
|
+
* @class InputAdapter
|
|
8
|
+
* @extends Event
|
|
9
|
+
*/
|
|
10
|
+
class InputAdapter extends Event {
|
|
11
|
+
/**
|
|
12
|
+
* Starts listening for input and emits an `input` event.
|
|
13
|
+
*
|
|
14
|
+
* @returns {void}
|
|
15
|
+
*/
|
|
16
|
+
start() {
|
|
17
|
+
this.emit('input',
|
|
18
|
+
InputMessage.from({ value: "Adapter started" })
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Stops listening for input. Default implementation does nothing.
|
|
24
|
+
*
|
|
25
|
+
* @returns {void}
|
|
26
|
+
*/
|
|
27
|
+
stop() {
|
|
28
|
+
// Default implementation – does nothing
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Checks whether the adapter is ready to receive input.
|
|
33
|
+
*
|
|
34
|
+
* @returns {boolean} Always true in base class.
|
|
35
|
+
*/
|
|
36
|
+
isReady() {
|
|
37
|
+
return true
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default InputAdapter
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, it } from "node:test"
|
|
2
|
+
import { strict as assert } from "node:assert"
|
|
3
|
+
import InputAdapter from "./InputAdapter.js"
|
|
4
|
+
|
|
5
|
+
describe("InputAdapter", () => {
|
|
6
|
+
it("should create instance", () => {
|
|
7
|
+
const adapter = new InputAdapter()
|
|
8
|
+
assert.ok(adapter instanceof InputAdapter)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it("should start and emit input message", () => {
|
|
12
|
+
const adapter = new InputAdapter()
|
|
13
|
+
let emitted = false
|
|
14
|
+
let message = null
|
|
15
|
+
|
|
16
|
+
adapter.on('input', (msg) => {
|
|
17
|
+
emitted = true
|
|
18
|
+
message = msg
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
adapter.start()
|
|
22
|
+
assert.ok(emitted)
|
|
23
|
+
assert.ok(message)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it("should stop without error", () => {
|
|
27
|
+
const adapter = new InputAdapter()
|
|
28
|
+
assert.doesNotThrow(() => adapter.stop())
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it("should be ready by default", () => {
|
|
32
|
+
const adapter = new InputAdapter()
|
|
33
|
+
assert.ok(adapter.isReady())
|
|
34
|
+
})
|
|
35
|
+
})
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { notEmpty } from "@nan0web/types"
|
|
2
|
+
import { Message } from "@nan0web/co"
|
|
3
|
+
|
|
4
|
+
/** @typedef {Message | string | null} InputMessageValue */
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Represents a message input with value, options, and metadata.
|
|
8
|
+
*/
|
|
9
|
+
export default class InputMessage {
|
|
10
|
+
static ESCAPE = String.fromCharCode(27)
|
|
11
|
+
/** @type {InputMessageValue} Input value */
|
|
12
|
+
value
|
|
13
|
+
|
|
14
|
+
/** @type {string[]} Available options for this input */
|
|
15
|
+
options
|
|
16
|
+
|
|
17
|
+
/** @type {boolean} Whether this input is waiting for response */
|
|
18
|
+
waiting
|
|
19
|
+
|
|
20
|
+
/** @type {number} Timestamp when input was created */
|
|
21
|
+
#time
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Creates a new InputMessage instance.
|
|
25
|
+
* @param {object} props - Input message properties
|
|
26
|
+
* @param {InputMessageValue} [props.value=""] - Input value
|
|
27
|
+
* @param {string[]|string} [props.options=[]] - Available options
|
|
28
|
+
* @param {boolean} [props.waiting=false] - Waiting state flag
|
|
29
|
+
* @param {boolean} [props.escaped=false] - Sets value to escape when true
|
|
30
|
+
*/
|
|
31
|
+
constructor(props = {}) {
|
|
32
|
+
if ("string" === typeof props) {
|
|
33
|
+
props = { value: props }
|
|
34
|
+
}
|
|
35
|
+
const {
|
|
36
|
+
value = "",
|
|
37
|
+
waiting = false,
|
|
38
|
+
options = [],
|
|
39
|
+
escaped = false,
|
|
40
|
+
} = props
|
|
41
|
+
this.#time = Date.now()
|
|
42
|
+
this.waiting = Boolean(waiting)
|
|
43
|
+
|
|
44
|
+
// Properly handle string options by converting to array
|
|
45
|
+
this.options = Array.isArray(options) ? options.map(String) : [String(options)]
|
|
46
|
+
this.value = String(value)
|
|
47
|
+
if (!this.value && escaped) {
|
|
48
|
+
this.value = this.ESCAPE
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Checks if the input value is empty.
|
|
54
|
+
* @returns {boolean} True if value is empty or null, false otherwise
|
|
55
|
+
*/
|
|
56
|
+
get empty() {
|
|
57
|
+
return null === this.value || 0 === String(this.value).length
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Gets the timestamp when input was created.
|
|
62
|
+
* @returns {number} Creation timestamp
|
|
63
|
+
*/
|
|
64
|
+
get time() {
|
|
65
|
+
return this.#time
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Returns the escape value.
|
|
70
|
+
* @returns {string}
|
|
71
|
+
*/
|
|
72
|
+
get ESCAPE() {
|
|
73
|
+
return /** @type {typeof InputMessage} */ (this.constructor).ESCAPE
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Checks if the input is an escape sequence.
|
|
78
|
+
* @returns {boolean} True if input value is escape sequence, false otherwise
|
|
79
|
+
*/
|
|
80
|
+
get escaped() {
|
|
81
|
+
return this.ESCAPE === this.value
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Validates if the input has a non-empty value.
|
|
86
|
+
* @returns {boolean} True if input is valid, false otherwise
|
|
87
|
+
*/
|
|
88
|
+
get isValid() {
|
|
89
|
+
// An input is valid only if it has a non-empty value and is not an escape sequence
|
|
90
|
+
return notEmpty(this.value) && this.value !== this.ESCAPE
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Converts the input to a plain object representation.
|
|
95
|
+
* @returns {object} Object with all properties including timestamp
|
|
96
|
+
*/
|
|
97
|
+
toObject() {
|
|
98
|
+
return { ...this, time: this.time }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Converts the input to a string representation including timestamp.
|
|
103
|
+
* @returns {string} String representation with timestamp and value
|
|
104
|
+
*/
|
|
105
|
+
toString() {
|
|
106
|
+
const date = new Date(this.time)
|
|
107
|
+
return `${date.toISOString().split(".")[0]} ${this.value}`
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Creates an InputMessage instance from the given value.
|
|
112
|
+
* @param {InputMessage|object|string} value - The value to create from
|
|
113
|
+
* @returns {InputMessage} An InputMessage instance
|
|
114
|
+
*/
|
|
115
|
+
static from(value) {
|
|
116
|
+
if (value instanceof InputMessage) return value
|
|
117
|
+
return new this(value)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, it } from "node:test"
|
|
2
|
+
import { strict as assert } from "node:assert"
|
|
3
|
+
import InputMessage from "./InputMessage.js"
|
|
4
|
+
import { notEmpty, empty } from "@nan0web/types"
|
|
5
|
+
|
|
6
|
+
describe("InputMessage", () => {
|
|
7
|
+
it("should create instance with default values", () => {
|
|
8
|
+
const msg = new InputMessage()
|
|
9
|
+
assert.ok(msg instanceof InputMessage)
|
|
10
|
+
assert.equal(msg.value, "")
|
|
11
|
+
assert.equal(msg.waiting, false)
|
|
12
|
+
assert.deepEqual(msg.options, [])
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it("should create instance with custom values", () => {
|
|
16
|
+
const props = {
|
|
17
|
+
value: "user input",
|
|
18
|
+
waiting: true,
|
|
19
|
+
options: ["option1", "option2"]
|
|
20
|
+
}
|
|
21
|
+
const msg = new InputMessage(props)
|
|
22
|
+
assert.equal(msg.value, "user input")
|
|
23
|
+
assert.equal(msg.waiting, true)
|
|
24
|
+
assert.deepEqual(msg.options, ["option1", "option2"])
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it("should handle string options correctly", () => {
|
|
28
|
+
const msg = new InputMessage({ options: "single-option" })
|
|
29
|
+
assert.deepEqual(msg.options, ["single-option"])
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it("should detect empty value", () => {
|
|
33
|
+
const emptyMsg = new InputMessage({ value: "" })
|
|
34
|
+
const nonEmptyMsg = new InputMessage({ value: "test" })
|
|
35
|
+
assert.ok(emptyMsg.empty)
|
|
36
|
+
assert.ok(!nonEmptyMsg.empty)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it("should validate message", () => {
|
|
40
|
+
const validMsg = new InputMessage({ value: "test" })
|
|
41
|
+
const invalidMsg = new InputMessage({ value: "" })
|
|
42
|
+
assert.ok(validMsg.isValid)
|
|
43
|
+
assert.ok(!invalidMsg.isValid)
|
|
44
|
+
})
|
|
45
|
+
})
|