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">
import { Index, MaybeIndex, equals } from '../../util';
import NicerForm, { Fields } from './NicerForm.vue';
import { Fields } from './NicerForm.vue';
export type Variant = {
fields?: Fields,
@ -21,9 +21,11 @@ const props = withDefaults(defineProps<{
// One-Way Bindings
variants?: Variants,
label?: string,
}>(), {
modelValue: null,
variants: () => ({}),
label: '',
});
const { variants } = $(props);
@ -64,14 +66,15 @@ watchEffect(() => {
</script>
<template>
<div class="enum-input">
<div class="form">
<label v-text="label"/>
<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"/>
<template v-else>{{ variant.display }}</template>
</button>
</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>
</template>
<style scoped>
@ -79,9 +82,6 @@ watchEffect(() => {
flex-flow: nowrap;
gap: 0.25rem;
}
.variant { padding: 0.25rem; gap: 0.25rem; }
.selected { background-color: var(--cl-bg-sl); }
.enum-fields {
padding-left: 0px;
}
.pillbar > button { padding: 0.25rem; gap: 0.25rem; }
.pillbar > .selected { background-color: var(--cl-bg-sl); }
</style>

View file

@ -50,21 +50,12 @@ watch($$(modelValue), (val) => {
<template>
<div class="form">
<component v-if="heading" :is="`h${headingLevel}`" :text="heading"/>
<template v-for="[index, field] of Object.entries(fields)" :key="index">
<label v-if="field.label" v-text="field.label"/>
<component :is="field.is" v-model="modelValue[index]" v-bind="field.props"/>
</template>
<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">
<label v-if="field.label && field.is !== 'EnumInput'" v-text="field.label"/>
<component :is="field.is" v-model="modelValue[index]" v-bind="field.is === 'EnumInput' ? Object.assign({label: field.label}, field.props) : field.props"/>
</template>
</div>
</div>
</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) => {
let res = await apiCall('Object.services.list', {});
let res = await apiCall('object.services.list', {});
if (res.Error === null) {
console.debug('services', res.Data);
let obj = {} as Options;

View file

@ -25,19 +25,32 @@ input {
padding: 0.25rem;
}
form, .form {
/* Universal Form Style-Component */
:is(form, .form) {
display: grid;
grid-template-columns: auto 1fr;
padding: 0.5rem;
gap: 0.5rem;
}
:is(form, .form) > :is(button, .button, h1) {
grid-column: 1 / 3;
:is(form, .form) :is(form, .form) { /* Subform and EnumInput */
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 {
grid-column: 1;
grid-column: 1; /* Re-align unbalanced grid => allows the last column(s) to be optional */
padding: 0.25rem;
}
:is(form, .form) > label::after {
content: ":";
}
table {
width: 100%;

View file

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

View file

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