base/src/form.js

122 lines
6.5 KiB
JavaScript
Raw Normal View History

import { Component, NewState, Util, $ } from '@web/state'
import { HTTP } from './http.js'
Component.register('AutoForm', container => {
if (!container.state.schema) container.state.schema = []
container.vertical = container.hasAttribute('vertical')
container.request = { method: 'POST' }
container.response = {}
container.result = null
container.data = NewState(container.data || {})
container.form = $(container, 'form')
container.submit = (opt = {}) => {
if (!container.form.reportValidity()) return globalThis.UI?.toast?.('{#verify failed#}', { type: 'danger' })
if (!container.dispatchEvent(new CustomEvent('submit', { detail: container.data, cancelable: true, bubbles: false }))) return
const req = { ...container.request, data: container.data, noui: true, ...opt }
let task = null
if (container.api) task = container.api.do(req)
else if (container.request.url) task = HTTP.request(req)
else return console.warn('{#please config .api or .request.url to auto submit#}')
task.then(resp => {
container.response = resp
container.result = resp.result
if (typeof resp.result === 'object' && resp.result.error) throw new Error(resp.result.error)
container.dispatchEvent(new CustomEvent('response', { detail: resp, bubbles: false }))
}).catch(err => {
if (globalThis.UI?.toast) UI.toast(err.message, { type: 'danger' })
container.dispatchEvent(new CustomEvent('error', { detail: err, bubbles: true }))
})
}
}, Util.makeDom(/*html*/`
<div>
<form $class="align-items-center \${this.vertical?'':'auto-grid-form'}" $onsubmit="event.stopPropagation();event.preventDefault();this.submit()">
<div $each="this.state.schema || []" style="display:contents">
<label $name="item.name" class="col-form-label text-muted" $text="item.label"></label>
<div control-wrapper class="mb-3">
<input $if="['text', 'password', 'email', 'number', 'date', 'datetime', 'file'].includes(item.type)" $name="item.name" class="form-control" $type="item.type" $.="item.setting" $bind="this.data[item.name]">
<select $if="item.type === 'select'" $name="item.name" class="form-select" $.="item.setting" $bind="this.data[item.name]">
<option value="" $if="item.placeholder" $text="item.placeholder" disabled selected></option>
<option $each="item.options" $value="item.value || item" $text="item.label || item"></option>
</select>
<div $if="['checkbox', 'radio'].includes(item.type)" >
<label $each="item.options || [item.text||item.label||item.name]" as="option" $class="form-check\${item.vertical ? '' : ' form-check-inline'}">
<input $name="item.name" class="form-check-input" $type="item.type" $.="item.setting" $value="item.options?option:'on'" $bind="this.data[item.name]">
<span $text="option" class="form-check-label"></span>
</label>
</div>
<div $if="item.type==='switch'" class="form-check form-switch fs-4"><input $name="item.name" class="form-check-input my-0" type="checkbox" $bind="this.data[item.name]"></div>
<textarea $if="item.type==='textarea'" $name="item.name" class="form-control" $.="item.setting" $bind="this.data[item.name]"></textarea>
</div>
</div>
<div class="d-flex justify-content-end align-items-baseline gap-3 mt-3" style="grid-column:1/-1">
<div slot-id="actions"></div>
<button type="submit" class="btn btn-primary" $text="this.submitlabel || '{#Submit#}'"></button>
</div>
</form>
</div>
`), Util.makeDom(/*html*/`<style>@media (min-width: 576px) { .auto-grid-form {display: grid;grid-template-columns: max-content 1fr} .auto-grid-form .col-form-label {text-align: right; margin-bottom: 1rem;padding-right: 1rem;max-width: 200px} }</style>`))
const _pendingAutoFormComponents = []
export const AutoForm = {
register: name => {
if (typeof document !== 'undefined') {
if (document.readyState !== 'loading' && Component.getTemplate('AutoForm')) AutoForm._addAutoFormComponent(name)
else _pendingAutoFormComponents.push(name)
}
},
_addAutoFormComponent: name => {
const template = Component.getTemplate('AutoForm')
if (template) {
$(template.content, '[control-wrapper]')?.appendChild(Util.makeDom(`<${name} $if="item.type.toUpperCase() === '${name.toUpperCase()}'" $name="item.name" $.="item.setting" $bind="this.data[item.name]"></${name}>`))
} else if (document.readyState !== 'loading') {
// If template still missing after DOM ready, something is wrong
console.error('AutoForm template not found during registration of', name)
}
}
}
if (typeof document !== 'undefined') {
const initAutoForm = () => {
_pendingAutoFormComponents.forEach(name => AutoForm._addAutoFormComponent(name))
_pendingAutoFormComponents.length = 0
}
if (document.readyState !== 'loading') setTimeout(initAutoForm, 100) // Wait slightly for component templates to be added to body
else document.addEventListener('DOMContentLoaded', () => setTimeout(initAutoForm, 100), true)
}
Component.register('TagsInput', container => {
container.addEventListener('bind', e => {
container.state.tags = e.detail || []
})
}, Util.makeDom(/*html*/`
<div class="form-control d-flex flex-wrap gap-1 align-items-center" style="min-height:38px;cursor:text">
<button $each="this.state.tags" type="button" class="btn btn-sm btn-outline-primary rounded-pill py-0 px-2" $onkeydown="${Util.getFunctionBody(event => {
if (['Backspace', 'Delete'].includes(event.key)) {
event.preventDefault()
this.state.tags.splice(index, 1)
this.state.tags = this.state.tags
this.dispatchEvent(new CustomEvent('change', { bubbles: false, detail: this.state.tags }))
Promise.resolve().then(() => {
const buttons = $$(this, 'button');
(buttons.length > 0 ? buttons[index > 0 ? index - 1 : 0] : $(this, 'input')).focus()
})
}
})}" $text="item"></button>
<input type="text" class="border-0 shadow-none py-0 px-2 flex-grow-1 bg-transparent" placeholder="{#new tag name#}" style="min-width:100px;width:0;outline:none" $onkeydown="${Util.getFunctionBody(event => {
if (event.isComposing) return
if (['Enter', ',', ' '].includes(event.key)) {
event.preventDefault()
const v = thisNode.value.trim()
if (v && !this.state.tags.includes(v)) {
this.state.tags.push(v)
this.state.tags = this.state.tags
this.dispatchEvent(new CustomEvent('change', { bubbles: false, detail: this.state.tags }))
}
thisNode.value = ''
}
})}">
</div>
`), Util.makeDom(/*html*/`<style>TagsInput button:focus {background-color:var(--bs-btn-hover-bg);color:var(--bs-btn-hover-color)}</style>`))
AutoForm.register('TagsInput')