Enhance EnumInput and Forms

- Soft-Nesting Layout to show structure without too much x-overhead
- Forms now use subgrid where possible
- Generalized form style to cover more cases and remove redundancy
- Minor definitions fix
- Made NicerForm a global component
- Updated Test Page to use global components
This commit is contained in:
adro 2023-11-02 23:53:54 +01:00
parent 864ca6defd
commit bc83309d6d
6 changed files with 47 additions and 40 deletions

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { Index, MaybeIndex, equals } from '../../util'; import { Index, MaybeIndex, equals } from '../../util';
import NicerForm, { Fields } from './NicerForm.vue'; import { Fields } from './NicerForm.vue';
export type Variant = { export type Variant = {
fields?: Fields, fields?: Fields,
@ -21,9 +21,11 @@ const props = withDefaults(defineProps<{
// One-Way Bindings // One-Way Bindings
variants?: Variants, variants?: Variants,
label?: string,
}>(), { }>(), {
modelValue: null, modelValue: null,
variants: () => ({}), variants: () => ({}),
label: '',
}); });
const { variants } = $(props); const { variants } = $(props);
@ -64,14 +66,15 @@ watchEffect(() => {
</script> </script>
<template> <template>
<div class="enum-input"> <div class="form">
<label v-text="label"/>
<div class="pillbar"> <div class="pillbar">
<button class="variant" v-for="[index, variant] of Object.entries(variants)" :key="index" :class="{selected: currentVariant === index}" @click="() => currentVariant = index"> <button v-for="[index, variant] of Object.entries(variants)" :key="index" :class="{selected: currentVariant === index}" @click="() => currentVariant = index">
<component v-if="variant.icon" :is="variant.icon"/> <component v-if="variant.icon" :is="variant.icon"/>
<template v-else>{{ variant.display }}</template> <template v-else>{{ variant.display }}</template>
</button> </button>
</div> </div>
<NicerForm class="enum-fields" v-if="currentVariant && variants[currentVariant]?.fields" :fields="variants[currentVariant].fields" v-model="formValue" :key="currentVariant"/> <NicerForm v-if="currentVariant && variants[currentVariant]?.fields" :fields="variants[currentVariant].fields" v-model="formValue" :key="currentVariant"/>
</div> </div>
</template> </template>
<style scoped> <style scoped>
@ -79,9 +82,6 @@ watchEffect(() => {
flex-flow: nowrap; flex-flow: nowrap;
gap: 0.25rem; gap: 0.25rem;
} }
.variant { padding: 0.25rem; gap: 0.25rem; } .pillbar > button { padding: 0.25rem; gap: 0.25rem; }
.selected { background-color: var(--cl-bg-sl); } .pillbar > .selected { background-color: var(--cl-bg-sl); }
.enum-fields {
padding-left: 0px;
}
</style> </style>

View file

@ -50,21 +50,12 @@ watch($$(modelValue), (val) => {
<template> <template>
<div class="form"> <div class="form">
<component v-if="heading" :is="`h${headingLevel}`" :text="heading"/> <component v-if="heading" :is="`h${headingLevel}`">{{ heading }}</component>
<div class="form inner-form">
<template v-for="[index, field] of Object.entries(fields)" :key="index"> <template v-for="[index, field] of Object.entries(fields)" :key="index">
<label v-if="field.label" v-text="field.label"/> <label v-if="field.label && field.is !== 'EnumInput'" v-text="field.label"/>
<component :is="field.is" v-model="modelValue[index]" v-bind="field.props"/> <component :is="field.is" v-model="modelValue[index]" v-bind="field.is === 'EnumInput' ? Object.assign({label: field.label}, field.props) : field.props"/>
</template> </template>
</div> </div>
</div>
</template> </template>
<style scoped>
label::after {
content: ":";
}
label {
border-left: 1px solid var(--cl-fg);
border-bottom: 1px solid var(--cl-fg);
}
</style>

View file

@ -49,7 +49,7 @@ const GetAddresses: SearchProvider = async (o) => {
}; };
const GetServices: SearchProvider = async (o) => { const GetServices: SearchProvider = async (o) => {
let res = await apiCall('Object.services.list', {}); let res = await apiCall('object.services.list', {});
if (res.Error === null) { if (res.Error === null) {
console.debug('services', res.Data); console.debug('services', res.Data);
let obj = {} as Options; let obj = {} as Options;

View file

@ -25,19 +25,32 @@ input {
padding: 0.25rem; padding: 0.25rem;
} }
form, .form { /* Universal Form Style-Component */
:is(form, .form) {
display: grid; display: grid;
grid-template-columns: auto 1fr; grid-template-columns: auto 1fr;
padding: 0.5rem; padding: 0.5rem;
gap: 0.5rem; gap: 0.5rem;
} }
:is(form, .form) > :is(button, .button, h1) { :is(form, .form) :is(form, .form) { /* Subform and EnumInput */
grid-column: 1 / 3; grid-column: 1 / 3; /* Maintenance: This column-end must match the column count of the host form, +1 */
grid-template-columns: subgrid; /* All descendants of a form align to that same form */
padding: 0px; /* To keep alignment, no padding is needed for those descendants */
}
:is(form, .form) :is(form, .form) > .inner-form { /* Soft-Nesting for Subform and EnumInput */
border-left: 1px solid var(--cl-fg);
padding-left: 0.5rem;
}
:is(form, .form) > :is(button, .button, h1, h2, h3, h4, h5, h6) {
grid-column: 1 / 3; /* Full-Span children */
} }
:is(form, .form) > label { :is(form, .form) > label {
grid-column: 1; grid-column: 1; /* Re-align unbalanced grid => allows the last column(s) to be optional */
padding: 0.25rem; padding: 0.25rem;
} }
:is(form, .form) > label::after {
content: ":";
}
table { table {
width: 100%; width: 100%;

View file

@ -5,6 +5,7 @@ import './global-styles/mlfe.css';
import './global-styles/transitions.css'; import './global-styles/transitions.css';
import 'vue-toast-notification/dist/theme-default.css'; import 'vue-toast-notification/dist/theme-default.css';
import NicerForm from './components/inputs/NicerForm.vue';
import PillBar from './components/inputs/PillBar.vue'; import PillBar from './components/inputs/PillBar.vue';
import TextBox from './components/inputs/TextBox.vue'; import TextBox from './components/inputs/TextBox.vue';
import EnumInput from './components/inputs/EnumInput.vue'; import EnumInput from './components/inputs/EnumInput.vue';
@ -34,6 +35,7 @@ app.use(head);
app.use(ToastPlugin); app.use(ToastPlugin);
// Global Components // Global Components
app.component('NicerForm', NicerForm);
app.component('PillBar', PillBar); app.component('PillBar', PillBar);
app.component('TextBox', TextBox); app.component('TextBox', TextBox);
app.component('NumberBox', NumberBox); app.component('NumberBox', NumberBox);

View file

@ -1,10 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import SingleSelect from '../components/inputs/SingleSelect.vue';
import { SearchProvider, Options } from '../components/inputs/DropdownInput.vue'; import { SearchProvider, Options } from '../components/inputs/DropdownInput.vue';
import MultiSelect from '../components/inputs/MultiSelect.vue';
import NicerForm from '../components/inputs/NicerForm.vue';
import EnumInput from '../components/inputs/EnumInput.vue';
import TextBox from '../components/inputs/TextBox.vue';
const testValues: Options = { const testValues: Options = {
1: { display: 'Option 1' }, 1: { display: 'Option 1' },
@ -28,18 +23,24 @@ function genSP(indexIsChar: boolean): SearchProvider {
<div> <div>
<PageHeader title="Test Page"/> <PageHeader title="Test Page"/>
<NicerForm :fields="{ <NicerForm :fields="{
Single: { is: SingleSelect, label: 'SingleSelect', props: { options: testValues, searchProvider: genSP(true) } }, Single: { is: 'SingleSelect', label: 'SingleSelect', props: { options: testValues, searchProvider: genSP(true) } },
Multiple: { is: MultiSelect, label: 'Multiselect', props: { options: testValues, searchProvider: genSP(false) } }, Multiple: { is: 'MultiSelect', label: 'Multiselect', props: { options: testValues, searchProvider: genSP(false) } },
IP: { is: EnumInput, label: 'IP Address', props: { variants: { IP: { is: 'EnumInput', label: 'IP Address', props: { variants: {
'dhcp': { display: 'DHCP' }, 'dhcp': { display: 'DHCP' },
'static-ipv4': { 'static-ipv4': {
display: 'Static (IPV4)', display: 'Static (IPV4)',
fields: { fields: {
ip: { is: TextBox, label: 'IP Address + CIDR' }, ip: { is: 'TextBox', label: 'IP Address (CIDR)' },
gw: { is: TextBox, label: 'Gateway Address' }, gw: { is: 'TextBox', label: 'Gateway Address' },
}, },
}, },
}}}, }}},
Subform: { is: 'NicerForm', props: { heading: 'Subform', fields: {
Text: { is: 'TextBox', label: 'Text' },
Subform2: { is: 'NicerForm', props: { heading: 'Subform2', fields: {
Text: { is: 'TextBox', label: 'Text' },
} } },
} } },
}" v-model="vm"/> }" v-model="vm"/>
{{ vm }} {{ vm }}
<button @click="() => { vm.Multiple = [1]; }">Click me</button> <button @click="() => { vm.Multiple = [1]; }">Click me</button>