diff --git a/README.md b/README.md index b4a8e54..bcf635c 100644 --- a/README.md +++ b/README.md @@ -11,16 +11,17 @@ npm i @quasi-dev/autoform ## Demo ```typescript -import { AutoForm } from '@quasi-dev/autoform' -const af = new AutoForm({ - type: 'input', - hint: 'Input a string.' +import { AButton, AButtonModel } from '@quasi-dev/autoform' + +let el = document.getElementById('root') as HTMLDivElement + +let btn = new AButton(new AButtonModel()) +btn.mount(el) +btn.patch({ + caption: '123' }) -af.init(document.getElementById('root') as HTMLDivElement) ``` ## 文档 -[Api](./doc/api.md) - -[JSON模板格式](./doc/template.md) +暂缺 diff --git a/doc/api.md b/doc/api.md deleted file mode 100644 index 8428c35..0000000 --- a/doc/api.md +++ /dev/null @@ -1,88 +0,0 @@ -# Api - -目前 `@quasi-dev/autoform` 中只有 `AutoForm` 这一个对象 - -## 构造函数 - -创建 `AutoForm` 对象时需要一个 `json` 格式的模板,用于指定表单的内容及组成。 - -示例代码: - -```ts -const af = new AutoForm({ - type: 'input', - hint: 'Input a string.' -}) -``` - -完整的模板格式参阅[JSON模板格式](./doc/template.md) - -## 显示 - -`AutoForm` 对象创建后,可以通过 `.init(el)` 方法绑定到页面中的一个元素上,要求 `el` 必须是 `HTMLDivElement`。 - -示例代码: - -```ts -af.init(document.getElementById('form') as HTMLDivElement) -``` - -## 获取表单数据 - -通过调用 `.value()` 方法可以获取表单数据,返回值是 `json` 格式的数据。 - -返回值类型可由下表确定: - -| 表单结构 | 返回值类型 | 说明 | -|:---:|:---:|:---:| -| 单行输入框 | `string` | 用户输入的内容 | -| 下拉选择框 | `string` | 选项的文本 | -| 复选框 | `boolean` | 是否选中 | -| 表单 | `json` | 子表单数据 | - -例如,当 `template` 为如下内容时: - -```ts -{ - type: 'form', - child: { - input1: { - type: 'input', - }, - input2: { - type: 'input' - }, - checkbox: { - type: 'checkbox', - label: 'click me' - }, - subform: { - type: 'form', - child: { - input3: { - type: 'input' - } - sel1: { - type: 'select', - option: ['option1', 'option2', 'option3'] - } - } - } -} -``` - -一个可能的返回值为: - -```ts -{ - input1: 'input1 value', - input2: 'input2 value', - checkbox: true, - subform: { - input3: 'input3 value', - sel1: 'option2' - } -} -``` - -暂时还不支持设置表单数据的功能 diff --git a/doc/template.md b/doc/template.md deleted file mode 100644 index 6387964..0000000 --- a/doc/template.md +++ /dev/null @@ -1,48 +0,0 @@ -# JSON模板格式 - -目前支持的控件有以下几个: - -* [单行输入框](#单行输入框) -* [下拉选择框](#下拉选择框) -* [复选框](#复选框) -* [表单](#表单) - -## 单行输入框 - -| 参数 | 类型 | 是否可选 | 说明 | -|:---:|:---:|:---:|:---:| -| caption | string | yes | [caption字段作用说明](#caption-字段作用说明) | -| type | 'input' | no | 标识字段 | -| default | string | yes | 默认值,尚未实现 | -| hint | string | yes | 提示文本 | -| validate | string => boolean | yes | 校验函数,尚未实现 | - -## 下拉选择框 - -| 参数 | 类型 | 是否可选 | 说明 | -|:---:|:---:|:---:|:---:| -| caption | string | yes | [caption字段作用说明](#caption-字段作用说明) | -| type | 'select' | no | 标识字段 | -| option | string[] | no | 选项列表 | - -## 复选框 - -| 参数 | 类型 | 是否可选 | 说明 | -|:---:|:---:|:---:|:---:| -| caption | string | yes | [caption字段作用说明](#caption-字段作用说明) | -| type | 'checkbox' | no | 标识字段 | -| label | string | yes | 选项文本 | - -## 表单 - -| 参数 | 类型 | 是否可选 | 说明 | -|:---:|:---:|:---:|:---:| -| caption | string | yes | [caption字段作用说明](#caption-字段作用说明) | -| type | 'form' | no | 标识字段 | -| child | JSON[] | no | 子控件列表 | - -## caption 字段作用说明 - -当一个控件单独出现时,设置它的 `caption` 没有意义。 - -当一个控件被包含在表单中时,`caption` 将作为这个控件的说明显示出来。 diff --git a/package.json b/package.json index 72a1fb9..7f7b43a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@quasi-dev/autoform", - "version": "0.0.1", + "version": "0.1.0", "type": "module", "license": "MIT", "description": "A simple form generator", @@ -10,7 +10,8 @@ "_KermanX" ], "scripts": { - "build": "tsc" + "build": "tsc", + "publish": "npm publish --access=public" }, "dependencies": { "mdui": "^1.0.2" @@ -18,8 +19,7 @@ "devDependencies": { "typescript": "^5.0.2" }, - "main": "./dist/src/index.js", - "types": "./dist/src/index.d.ts", + "main": "./src/index.ts", "files": [ "dist/src", "README.md", @@ -28,8 +28,7 @@ "exports": { ".": { "import": { - "types": "./dist/src/index.d.ts", - "default": "./dist/src/index.js" + "default": "./src/index.ts" } } } diff --git a/src/autoform.ts b/src/autoform.ts deleted file mode 100644 index 81fd126..0000000 --- a/src/autoform.ts +++ /dev/null @@ -1,29 +0,0 @@ -import mdui from "mdui"; -import { ABase, ElTree, ReturnValue } from "./types.js"; -import { inject } from "./inject.js"; -import { value } from "./value.js"; - -class AutoForm { - readonly template: T - // @ts-ignore - el_tree: ElTree - - constructor (template: T) { - this.template = template - } - - init(el: HTMLDivElement): void { - this.el_tree = inject(this.template) - el.appendChild(this.el_tree.div) - mdui.mutation() - console.log(this.el_tree) - } - - value(): ReturnValue { - return value(this.el_tree, this.template) - } -} - -export { - AutoForm -} \ No newline at end of file diff --git a/src/component/base.ts b/src/component/base.ts new file mode 100644 index 0000000..03d0d1f --- /dev/null +++ b/src/component/base.ts @@ -0,0 +1,27 @@ +abstract class ComponentModel { + abstract update(payload: any, forward: (msg: MsgType) => void): void +} + +abstract class ComponentBase, MsgType> { + el: HTMLDivElement + // model: Model + + constructor(public model: Model) { + this.el = document.createElement('div') + // this.model = new componentModelCtor() + } + + public patch(payload: any): void { + this.model.update(payload, this.update.bind(this)) + } + + public abstract mount(el: HTMLDivElement): void + + protected abstract update(msg: MsgType): void +} + + +export { + ComponentModel, + ComponentBase +} \ No newline at end of file diff --git a/src/component/button.ts b/src/component/button.ts new file mode 100644 index 0000000..b3e0a24 --- /dev/null +++ b/src/component/button.ts @@ -0,0 +1,52 @@ +import mdui from "mdui" +import { ComponentBase, ComponentModel } from ".." + +type AButtonMsgType = 'caption' + +class AButtonModel extends ComponentModel { + caption: string = '' + onclick: () => void = () => {} + + update(payload: any, forward: (msg: AButtonMsgType) => void): void { + if (payload.caption !== undefined) { + this.caption = payload.caption + forward('caption') + } + + if (payload.onclick !== undefined) { + this.onclick = payload.onclick + } + } +} + +class AButton extends ComponentBase { + + button_el: HTMLButtonElement + + mount(el: HTMLDivElement): void { + this.button_el = document.createElement('button') + this.button_el.classList.add('mdui-btn') + this.button_el.classList.add('mdui-btn-raised') + this.button_el.classList.add('mdui-ripple') + this.update('caption') + this.el.appendChild(this.button_el) + + this.button_el.addEventListener('click', (_) => { + this.model.onclick() + }) + + el.appendChild(this.el) + } + + update(msg: AButtonMsgType): void { + if (msg === 'caption') { + this.button_el.innerText = this.model.caption + } + mdui.mutation() + } +} + +export { + AButtonModel, + AButton +} \ No newline at end of file diff --git a/src/component/checkbox.ts b/src/component/checkbox.ts new file mode 100644 index 0000000..1ecb089 --- /dev/null +++ b/src/component/checkbox.ts @@ -0,0 +1,78 @@ +import mdui from "mdui" +import { ComponentBase, ComponentModel } from ".." + +type ACheckboxMsgType = 'value' | 'label' + +type ACheckboxStatus = 'checked' | 'unchecked' // | 'partial' + +class ACheckboxModel extends ComponentModel { + value: ACheckboxStatus = 'unchecked' + label: string = '' + onchange: () => void = () => {} + + update(payload: any, forward: (msg: ACheckboxMsgType) => void): void { + if (payload.value !== undefined) { + this.value = payload.value + forward('value') + } + if (payload.label !== undefined) { + this.label = payload.label + forward('label') + } + if (payload.onchange !== undefined) { + this.onchange = payload.onchange + } + } +} + +class ACheckbox extends ComponentBase { + + label_el: HTMLLabelElement + input_el: HTMLInputElement + i_el: HTMLElement + + mount(el: HTMLDivElement): void { + this.label_el = document.createElement('label') + this.label_el.classList.add('mdui-checkbox') + // this.label_el.textContent = this.model.label + + this.input_el = document.createElement('input') + this.input_el.type = 'checkbox' + this.label_el.appendChild(this.input_el) + + this.i_el = document.createElement('i') + this.i_el.classList.add('mdui-checkbox-icon') + this.label_el.appendChild(this.i_el) + + this.label_el.appendChild(new Text(this.model.label)) + this.update('value') + this.input_el.addEventListener('change', () => { + this.model.value = this.input_el.checked ? 'checked': 'unchecked' + this.update('value') + this.model.onchange() + } ) + this.el.appendChild(this.label_el) + + el.appendChild(this.el) + } + + update(msg: ACheckboxMsgType): void { + if (msg === 'label') { + // this.label_el.textContent = this.model.label + // this.label_el.appendChild(new Text(this.model.label)) + (this.label_el.lastChild as Text).textContent = this.model.label + // this.label_el.appendChild(this.input_el) + // this.label_el.appendChild(this.i_el) + return + } + if (msg === 'value') { + this.input_el.checked = this.model.value === 'checked' + } + mdui.mutation() + } +} + +export { + ACheckboxModel, + ACheckbox +} \ No newline at end of file diff --git a/src/component/input.ts b/src/component/input.ts new file mode 100644 index 0000000..f2189f5 --- /dev/null +++ b/src/component/input.ts @@ -0,0 +1,66 @@ +import mdui from 'mdui' +import { ComponentBase, ComponentModel } from './base' + +type AInputMsgType = 'val' | 'place_holder' + +class AInputModel extends ComponentModel { + val: string = '' + place_holder: string = '' + onchange: () => void = () => {} + + update(payload: any, forward: (msg: AInputMsgType) => void): void { + if (payload.val !== undefined) { + this.val = payload.val + forward('val') + } + if (payload.place_holder !== undefined) { + this.place_holder = payload.place_holder + forward('place_holder') + } + if (payload.onchange !== undefined) { + this.onchange = payload.onchange + } + } +} + +class AInput extends ComponentBase { + + label_el: HTMLLabelElement + input_el: HTMLInputElement + + mount(el: HTMLDivElement): void { + this.el.classList.add('mdui-textfield') + this.el.classList.add('mdui-textfield-floating-label') + + this.label_el = document.createElement('label') + this.label_el.classList.add('mdui-textfield-label') + this.update('place_holder') + this.el.appendChild(this.label_el) + + this.input_el = document.createElement('input') + this.input_el.classList.add('mdui-textfield-input') + this.update('val') + this.el.appendChild(this.input_el) + + this.input_el.addEventListener('input', (_: Event) => { + this.model.val = this.input_el.value + this.model.onchange() + }) + el.appendChild(this.el) + } + + update(msg: AInputMsgType): void { + if (msg === 'val') { + this.input_el.value = this.model.val + mdui.updateTextFields(this.input_el) + } + if (msg === 'place_holder') + this.label_el.innerText = this.model.place_holder + mdui.mutation() + } +} + +export { + AInputModel, + AInput +} \ No newline at end of file diff --git a/src/component/select.ts b/src/component/select.ts new file mode 100644 index 0000000..522493e --- /dev/null +++ b/src/component/select.ts @@ -0,0 +1,70 @@ + +import mdui from "mdui" +import { ComponentBase, ComponentModel } from ".." + +type ASelectMsgType = 'options' + +class ASelectModel extends ComponentModel { + options: string[] = [] + value: string = '' + onchange: () => void = () => {} // after change + + update(payload: any, forward: (msg: ASelectMsgType) => void): void { + if (payload.options !== undefined) { + this.options = (payload.options as Array).filter(() => true) + forward('options') + } + if (payload.onchange !== undefined) { + this.onchange = payload.onchange + } + } +} + +class ASelect extends ComponentBase { + + select_el: HTMLSelectElement + option_el: HTMLOptionElement[] = [] + mdui_select_obj: any + + mount(el: HTMLDivElement): void { + this.select_el = document.createElement('select') + // this.select_el.classList.add('mdui-select') + // this.select_el.setAttribute('mdui-select', `{position: 'bottom'}`) + // I don't know why but it works. + this.mdui_select_obj = new mdui.Select(this.select_el) + this.select_el.addEventListener('change', () => { + this.model.value = this.select_el.value + this.model.onchange() + }) + this.el.appendChild(this.select_el) + this.update('options') + // mdui.mutation() + + el.appendChild(this.el) + } + + update(msg: ASelectMsgType): void { + if (msg === 'options') { + for (let i of this.option_el) + this.select_el.removeChild(i) + this.option_el = this.model.options.map((v, indx) => { + let option_el = document.createElement('option') + option_el.value = indx.toString() + option_el.innerText = v + this.select_el.appendChild(option_el) + return option_el + }) + if (parseInt(this.model.value) < this.model.options.length) { + this.select_el.value = this.model.value + } else { + this.select_el.value = '0' + } + this.mdui_select_obj.handleUpdate() + } + } +} + +export { + ASelectModel, + ASelect +} \ No newline at end of file diff --git a/src/component/switch.ts b/src/component/switch.ts new file mode 100644 index 0000000..f8ae498 --- /dev/null +++ b/src/component/switch.ts @@ -0,0 +1,63 @@ +import mdui from "mdui" +import { ComponentBase, ComponentModel } from ".." + +type ASwitchMsgType = 'value' + +type ASwitchStatus = 'off' | 'on' + +class ASwitchModel extends ComponentModel { + value: ASwitchStatus = 'off' + onchange: () => void = () => {} + + update(payload: any, forward: (msg: ASwitchMsgType) => void): void { + if (payload.value !== undefined) { + this.value = payload.value + forward('value') + } + if (payload.onchange !== undefined) { + this.onchange = payload.onchange + } + } +} + +class ASwitch extends ComponentBase { + + label_el: HTMLLabelElement + input_el: HTMLInputElement + i_el: HTMLElement + + mount(el: HTMLDivElement): void { + this.label_el = document.createElement('label') + this.label_el.classList.add('mdui-switch') + + this.input_el = document.createElement('input') + this.input_el.type = 'checkbox' + this.label_el.appendChild(this.input_el) + + this.i_el = document.createElement('i') + this.i_el.classList.add('mdui-switch-icon') + this.label_el.appendChild(this.i_el) + + this.update('value') + this.input_el.addEventListener('change', () => { + this.model.value = this.input_el.checked ? 'on': 'off' + this.update('value') + this.model.onchange() + } ) + this.el.appendChild(this.label_el) + + el.appendChild(this.el) + } + + update(msg: ASwitchMsgType): void { + if (msg === 'value') { + this.input_el.checked = this.model.value === 'on' + } + mdui.mutation() + } +} + +export { + ASwitchModel, + ASwitch +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 6e9c7d2..3096015 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ -import { AutoForm } from "./autoform"; - -export { - AutoForm -} \ No newline at end of file +export * from './component/base' +export * from './component/input' +export * from './component/button' +export * from './component/select' +export * from './component/checkbox' +export * from './component/switch' \ No newline at end of file diff --git a/src/inject.ts b/src/inject.ts deleted file mode 100644 index fa06d08..0000000 --- a/src/inject.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { ABase, ASelect, AInput, ElASelect, ElAInput, ElTree, isASelect, isAInput, ACheckbox, ElACheckbox, isACheckbox, AForm, ElAForm, isAForm } from "./types.js" - -function inject_input(template: AInput): ElAInput { - let div = document.createElement('div') - div.classList.add('mdui-textfield', 'mdui-textfield-floating-label') - - let label = document.createElement('label') - label.classList.add('mdui-textfield-label') - if (template.hint) - label.innerText = template.hint - div.appendChild(label) - - let input = document.createElement('input') - input.classList.add('mdui-textfield-input') - div.appendChild(input) - - return { - div, label, input - } -} - -function inject_select(template: ASelect): ElASelect { - let div = document.createElement('div') - - let select = document.createElement('select') - select.classList.add('mdui-select') - select.setAttribute('mdui-select', `{position: 'bottom'}`) - div.appendChild(select) - - let options: HTMLOptionElement[] = [] - for (let i = 0; i < template.option.length; i++) { - let option = document.createElement('option') - option.innerText = template.option[i] - option.setAttribute('value', i.toString()) - select.appendChild(option) - options.push(option) - } - - return { - div, select, - option: options - } -} - -function inject_checkbox(template: ACheckbox): ElACheckbox { - let div = document.createElement('div') - - let label = document.createElement('label') - label.classList.add('mdui-checkbox') - div.appendChild(label) - label.innerText = template.label - - let input = document.createElement('input') - input.setAttribute('type', 'checkbox') - label.appendChild(input) - - let i = document.createElement('i') - i.classList.add('mdui-checkbox-icon') - label.appendChild(i) - - return { - div, label, input, i - } -} - -function inject_form>(template: AForm): ElAForm { - let div = document.createElement('div') - div.classList.add('md-form') - - let child = {} - - for (let el in template.child) { - let s_div = document.createElement('div') - s_div.classList.add('md-form-group') - div.appendChild(s_div) - - let label = document.createElement('label') - label.classList.add('md-input-container') - if (template.child[el].caption) - label.innerText = template.child[el].caption! - s_div.appendChild(label) - - let s_el = inject(template.child[el]) - s_div.appendChild(s_el.div) - // @ts-ignore - child[el as string] = { - div, label, - el: s_el - } - } - - return { - div, - // @ts-ignore - child - } -} - -function inject(template: T): ElTree { - if (isAInput(template)) - return inject_input(template) - if (isASelect(template)) - return inject_select(template) - if (isACheckbox(template)) - return inject_checkbox(template) - if (isAForm(template)) - return inject_form(template) - throw new Error('Not implemented') -} - -export { - inject -} \ No newline at end of file diff --git a/src/types.ts b/src/types.ts deleted file mode 100644 index b039ec2..0000000 --- a/src/types.ts +++ /dev/null @@ -1,137 +0,0 @@ -type AMap = Record> = { - input: [AInput, ElAInput, string], - select: [ASelect, ElASelect, string], - checkbox: [ACheckbox, ElACheckbox, boolean], - form: [AForm, ElAForm, { - [K in keyof Child]: AMap[Child[K]["type"]][2] - }] -} - -interface ElBase { - div: HTMLDivElement -} - -interface ElAInput extends ElBase { - label: HTMLLabelElement - input: HTMLInputElement -} - -interface ElASelect extends ElBase { - select: HTMLSelectElement - option: HTMLOptionElement[] -} - -interface ElACheckbox extends ElBase { - label: HTMLLabelElement - input: HTMLInputElement - i: HTMLElement -} - -type ChildOfAForm = - T extends AForm ? Child : never; - -interface ElAFormChild extends ElBase { - el: T - label: HTMLLabelElement -} - -interface ElAForm = Record> extends ElBase { - child: { - [K in keyof Child]: ElAFormChild - } -} - -type ElAElement - = ElAInput - | ElASelect - | ElACheckbox - | ElAForm - -type ElTree = AMap[T['type']][1]; - -export type { - ElAInput, - ElASelect, - ElACheckbox, - ElAForm, - ElAElement, - ElBase, - ElTree -} - -interface ABase { - type: keyof AMap; - caption?: string -} - -interface AInput extends ABase { - type: 'input' - default?: string - hint?: string - validate?: (value: string) => { - isValid: boolean - message?: string - } -} - -interface ASelect extends ABase { - type: 'select' - option: string[] -} - -interface ACheckbox extends ABase { - type: 'checkbox' - label: string -} - -interface AForm = Record> extends ABase { - type: 'form' - child: Child -} - -type ReturnValue = AMap[T['type']][2] - -export type { - ABase, - AInput, - ASelect, - ACheckbox, - AForm, - ReturnValue, - ChildOfAForm -} - -function isAInput(obj: any): obj is AInput { - return obj.type === 'input' -} - -function isASelect(obj: any): obj is ASelect { - return obj.type === 'select' && obj.option instanceof Array -} - -function isACheckbox(obj: any): obj is ACheckbox { - return obj.type === 'checkbox' -} - -function isAForm(obj: any): obj is AForm { - if (!obj.child) - return false - if (obj.type !== 'form') - return false - for (let i of Object.keys(obj.child)) - if (!isAElement(obj.child[i])) - return false - return true -} - -function isAElement(obj: any): obj is ABase { - return isAInput(obj) || isASelect(obj) || isACheckbox(obj) || isAForm(obj) -} - -export { - isAInput, - isASelect, - isACheckbox, - isAForm, - isAElement -} \ No newline at end of file diff --git a/src/value.ts b/src/value.ts deleted file mode 100644 index b883e2f..0000000 --- a/src/value.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { ABase, ACheckbox, AForm, AInput, ASelect, ElACheckbox, ElAForm, ElAInput, ElASelect, ElBase, ReturnValue, isACheckbox, isAForm, isAInput, isASelect } from "./types.js"; - -function value_input(el: ElAInput, _: AInput): string { - return el.input.value -} - -function value_select(el: ElASelect, t: ASelect): string { - return t.option[el.select.selectedIndex] -} - -function value_checkbox(el: ElACheckbox, _: ACheckbox): boolean { - return el.input.checked -} - -function value_form>(el: ElAForm, t: AForm): ReturnValue> { - let ret = {} as ReturnValue> - for (let c in t.child) - ret[c] = value(el.child[c].el, t.child[c]) - return ret -} - -function value(el: ElBase, t: ABase): any { - if (isAInput(t)) - return value_input(el as ElAInput, t) - if (isASelect(t)) - return value_select(el as ElASelect, t) - if (isACheckbox(t)) - return value_checkbox(el as ElACheckbox, t) - if (isAForm(t)) - return value_form(el as ElAForm, t) - throw new Error('Not Implemented.') -} - -export { - value -} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 0a10013..9fa4950 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,7 @@ "module": "ESNext", "lib": ["ES2020", "DOM", "DOM.Iterable"], "skipLibCheck": true, + "strictPropertyInitialization": false, // "moduleResolution": "NodeNext", "composite": true, @@ -20,7 +21,7 @@ "outDir": "dist" }, "include": [ - "src/*" + "src/**/*" ], "exclude": ["node_modules"] }