feat: Implement unified input component architecture with standardized margins and layout modes

This commit is contained in:
Juergen Kunz
2025-06-19 09:41:00 +00:00
parent 8fb5e2e2a2
commit 79b1a4ea9f
7 changed files with 482 additions and 357 deletions

View File

@ -4,6 +4,7 @@ import {
type TemplateResult,
DeesElement,
type CSSResult,
property,
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
@ -11,6 +12,7 @@ import { DeesInputCheckbox } from './dees-input-checkbox.js';
import { DeesInputText } from './dees-input-text.js';
import { DeesInputQuantitySelector } from './dees-input-quantityselector.js';
import { DeesInputRadio } from './dees-input-radio.js';
import { DeesInputDropdown } from './dees-input-dropdown.js';
import { DeesFormSubmit } from './dees-form-submit.js';
import { DeesTable } from './dees-table.js';
import { demoFunc } from './dees-form.demo.js';
@ -19,6 +21,7 @@ import { DeesInputIban } from './dees-input-iban.js';
// Unified set for form input types
const FORM_INPUT_TYPES = [
DeesInputCheckbox,
DeesInputDropdown,
DeesInputIban,
DeesInputText,
DeesInputQuantitySelector,
@ -28,6 +31,7 @@ const FORM_INPUT_TYPES = [
export type TFormInputElement =
| DeesInputCheckbox
| DeesInputDropdown
| DeesInputIban
| DeesInputText
| DeesInputQuantitySelector
@ -48,6 +52,13 @@ export class DeesForm extends DeesElement {
public changeSubject = new domtools.plugins.smartrx.rxjs.Subject();
public readyDeferred = domtools.plugins.smartpromise.defer();
/**
* Controls the layout mode of child input components
* When true, sets all child inputs to horizontal layout
*/
@property({ type: Boolean, reflect: true, attribute: 'horizontal-layout' })
public horizontalLayout: boolean = false;
public render(): TemplateResult {
return html`
<style>
@ -62,6 +73,7 @@ export class DeesForm extends DeesElement {
public async firstUpdated() {
const formChildren = this.getFormElements();
this.updateRequiredStatus();
this.updateChildrenLayoutMode();
for (const child of formChildren) {
child.changeSubject.subscribe(async () => {
@ -107,7 +119,7 @@ export class DeesForm extends DeesElement {
*/
public async collectFormData() {
const children = this.getFormElements();
const valueObject: { [key: string]: string | number | boolean | any[] } = {};
const valueObject: { [key: string]: string | number | boolean | any[] | { option: string; key: string; payload?: any } } = {};
for (const child of children) {
if (!child.key) {
console.log(`form element with label "${child.label}" has no key. skipping.`);
@ -202,4 +214,28 @@ export class DeesForm extends DeesElement {
}
});
}
/**
* Updates the layout mode of child input components based on form's horizontalLayout property
*/
private updateChildrenLayoutMode() {
const formChildren = this.getFormElements();
for (const child of formChildren) {
if ('layoutMode' in child) {
// The child's auto mode will detect this form's horizontal-layout attribute
(child as any).layoutMode = 'auto';
}
}
}
/**
* Called when properties change
*/
updated(changedProperties: Map<string, any>) {
super.updated(changedProperties);
if (changedProperties.has('horizontalLayout')) {
this.updateChildrenLayoutMode();
}
}
}

View File

@ -0,0 +1,184 @@
import {
DeesElement,
property,
css,
type CSSResult,
cssManager,
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
/**
* Base class for all dees-input components
* Provides unified margin system and layout mode support
*/
export abstract class DeesInputBase<T = any> extends DeesElement {
/**
* Layout mode for the input component
* - vertical: Traditional form layout (label on top)
* - horizontal: Inline layout (label position configurable)
* - auto: Detect from parent context
*/
@property({ type: String })
public layoutMode: 'vertical' | 'horizontal' | 'auto' = 'auto';
/**
* Position of the label relative to the input
*/
@property({ type: String })
public labelPosition: 'top' | 'left' | 'right' | 'none' = 'top';
/**
* Common properties for all inputs
*/
@property({ type: String })
public key: string;
@property({ type: String })
public label: string;
@property({ type: Boolean })
public required: boolean = false;
@property({ type: Boolean })
public disabled: boolean = false;
@property({ type: String })
public description: string;
/**
* Common styles for all input components
*/
public static get baseStyles(): CSSResult[] {
return [
css`
/* CSS Variables for consistent spacing */
:host {
--dees-input-spacing-unit: 8px;
--dees-input-vertical-gap: calc(var(--dees-input-spacing-unit) * 2); /* 16px */
--dees-input-horizontal-gap: calc(var(--dees-input-spacing-unit) * 2); /* 16px */
--dees-input-label-gap: var(--dees-input-spacing-unit); /* 8px */
}
/* Default vertical stacking mode (for forms) */
:host {
display: block;
margin: 0;
margin-bottom: var(--dees-input-vertical-gap);
}
/* Last child in container should have no bottom margin */
:host(:last-child) {
margin-bottom: 0;
}
/* Horizontal layout mode - activated by attribute */
:host([layout-mode="horizontal"]) {
display: inline-block;
margin: 0;
margin-right: var(--dees-input-horizontal-gap);
margin-bottom: 0;
}
:host([layout-mode="horizontal"]:last-child) {
margin-right: 0;
}
/* Auto mode - inherit from parent dees-form if present */
/* Label position variations */
:host([label-position="left"]) .input-wrapper {
display: grid;
grid-template-columns: auto 1fr;
gap: var(--dees-input-label-gap);
align-items: center;
}
:host([label-position="right"]) .input-wrapper {
display: grid;
grid-template-columns: 1fr auto;
gap: var(--dees-input-label-gap);
align-items: center;
}
:host([label-position="top"]) .input-wrapper {
display: block;
}
:host([label-position="none"]) dees-label {
display: none;
}
`,
];
}
/**
* Subject for value changes that all inputs should implement
*/
public changeSubject = new domtools.plugins.smartrx.rxjs.Subject<T>();
/**
* Called when the element is connected to the DOM
* Sets up layout mode detection
*/
async connectedCallback() {
await super.connectedCallback();
this.detectLayoutMode();
}
/**
* Detects the appropriate layout mode based on parent context
*/
private detectLayoutMode() {
if (this.layoutMode !== 'auto') {
this.setAttribute('layout-mode', this.layoutMode);
return;
}
// Check if parent is a form with horizontal layout
const parentForm = this.closest('dees-form');
if (parentForm && parentForm.hasAttribute('horizontal-layout')) {
this.setAttribute('layout-mode', 'horizontal');
} else {
this.setAttribute('layout-mode', 'vertical');
}
}
/**
* Updates the layout mode attribute when property changes
*/
updated(changedProperties: Map<string, any>) {
super.updated(changedProperties);
if (changedProperties.has('layoutMode')) {
this.detectLayoutMode();
}
if (changedProperties.has('labelPosition')) {
this.setAttribute('label-position', this.labelPosition);
}
}
/**
* Standard method for freezing input (disabling)
*/
public async freeze() {
this.disabled = true;
}
/**
* Standard method for unfreezing input (enabling)
*/
public async unfreeze() {
this.disabled = false;
}
/**
* Abstract method that child classes must implement to get their value
*/
public abstract getValue(): any;
/**
* Abstract method that child classes must implement to set their value
*/
public abstract setValue(value: any): void;
}

View File

@ -1,14 +1,13 @@
import {
customElement,
DeesElement,
type TemplateResult,
property,
html,
css,
cssManager,
type CSSResult,
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import { DeesInputBase } from './dees-input-base.js';
declare global {
interface HTMLElementTagNameMap {
@ -17,51 +16,33 @@ declare global {
}
@customElement('dees-input-checkbox')
export class DeesInputCheckbox extends DeesElement {
export class DeesInputCheckbox extends DeesInputBase<DeesInputCheckbox> {
// STATIC
public static demo = () => html`<dees-input-checkbox></dees-input-checkbox>`;
// INSTANCE
public changeSubject = new domtools.plugins.smartrx.rxjs.Subject();
@property({
type: String,
reflect: true,
})
public key: string;
@property({
type: String,
})
public label: string = 'Label';
@property({
type: Boolean,
})
public value: boolean = false;
@property({
type: Boolean,
})
public required: boolean = false;
@property({
type: Boolean
})
public disabled: boolean = false;
constructor() {
super();
this.labelPosition = 'right'; // Checkboxes default to label on the right
}
public render(): TemplateResult {
return html`
${domtools.elementBasic.styles}
<style>
public static styles = [
...DeesInputBase.baseStyles,
cssManager.defaultStyles,
css`
* {
box-sizing: border-box;
}
:host {
display: block;
position: relative;
margin: 20px 0px;
cursor: default;
}
:host(:hover) {
@ -69,21 +50,12 @@ export class DeesInputCheckbox extends DeesElement {
}
.maincontainer {
display: grid;
grid-template-columns: 25px auto;
padding: 5px 0px;
color: ${this.goBright ? '#333' : '#ccc'};
color: ${cssManager.bdTheme('#333', '#ccc')};
}
.maincontainer:hover {
${this.goBright ? '#000' : '#ccc'};
}
.label {
margin-left: 15px;
line-height: 25px;
font-size: 14px;
font-weight: normal;
color: ${cssManager.bdTheme('#000', '#fff')};
}
input:focus {
@ -94,12 +66,12 @@ export class DeesInputCheckbox extends DeesElement {
.checkbox {
transition: all 0.1s;
box-sizing: border-box;
border: 1px solid ${this.goBright ? '#CCC' : '#999'};
border: 1px solid ${cssManager.bdTheme('#CCC', '#999')};
border-radius: 2px;
height: 24px;
width: 24px;
display: inline-block;
background: ${this.goBright ? '#fafafa' : '#222'};
background: ${cssManager.bdTheme('#fafafa', '#222')};
}
.checkbox.selected {
@ -146,19 +118,25 @@ export class DeesInputCheckbox extends DeesElement {
img {
padding: 4px;
}
</style>
<div class="maincontainer" @click="${this.toggleSelected}">
<div class="checkbox ${this.value ? 'selected' : ''} ${this.disabled ? 'disabled' : ''}" tabindex="0">
${this.value
? html`
<span class="checkmark">
<div class="checkmark_stem"></div>
<div class="checkmark_kick"></div>
</span>
`
: html``}
`,
];
public render(): TemplateResult {
return html`
<div class="input-wrapper">
<div class="maincontainer" @click="${this.toggleSelected}">
<div class="checkbox ${this.value ? 'selected' : ''} ${this.disabled ? 'disabled' : ''}" tabindex="0">
${this.value
? html`
<span class="checkmark">
<div class="checkmark_stem"></div>
<div class="checkmark_kick"></div>
</span>
`
: html``}
</div>
</div>
<div class="label">${this.label}</div>
<dees-label .label=${this.label}></dees-label>
</div>
`;
}
@ -177,6 +155,14 @@ export class DeesInputCheckbox extends DeesElement {
this.changeSubject.next(this);
}
public getValue(): boolean {
return this.value;
}
public setValue(value: boolean): void {
this.value = value;
}
public focus(): void {
const checkboxDiv = this.shadowRoot.querySelector('.checkbox');
if (checkboxDiv) {

View File

@ -1,17 +1,16 @@
import {
customElement,
DeesElement,
type TemplateResult,
property,
state,
html,
css,
cssManager,
type CSSResult,
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import { demoFunc } from './dees-input-dropdown.demo.js';
import { DeesWindowLayer } from './dees-windowlayer.js';
import { DeesInputBase } from './dees-input-base.js';
declare global {
interface HTMLElementTagNameMap {
@ -20,20 +19,10 @@ declare global {
}
@customElement('dees-input-dropdown')
export class DeesInputDropdown extends DeesElement {
export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> {
public static demo = demoFunc;
// INSTANCE
public changeSubject = new domtools.plugins.smartrx.rxjs.Subject();
@property({
type: String,
reflect: true,
})
public label: string = 'Label';
@property()
public key: string;
@property()
public options: { option: string; key: string; payload?: any }[] = [];
@ -41,20 +30,21 @@ export class DeesInputDropdown extends DeesElement {
@property()
public selectedOption: { option: string; key: string; payload?: any } = null;
@property({
type: Boolean,
})
public required: boolean = false;
// Add value property for form compatibility
public get value() {
return this.selectedOption;
}
public set value(val: { option: string; key: string; payload?: any }) {
this.selectedOption = val;
}
@property({
type: Boolean,
})
public enableSearch: boolean = true;
@property({
type: Boolean,
})
public disabled: boolean = false;
@state()
public opensToTop: boolean = false;
@ -69,6 +59,7 @@ export class DeesInputDropdown extends DeesElement {
public isOpened = false;
public static styles = [
...DeesInputBase.baseStyles,
cssManager.defaultStyles,
css`
* {
@ -78,19 +69,13 @@ export class DeesInputDropdown extends DeesElement {
:host {
font-family: Roboto;
position: relative;
display: block;
color: ${cssManager.bdTheme('#222', '#fff')};
margin-bottom: 24px;
}
.maincontainer {
display: block;
}
.label {
font-size: 14px;
margin-bottom: 8px;
}
.selectedBox {
user-select: none;
@ -205,9 +190,10 @@ export class DeesInputDropdown extends DeesElement {
public render(): TemplateResult {
return html`
<div class="maincontainer" @keydown="${this.isOpened ? this.handleKeyDown : undefined}">
${this.label ? html`<div class="label">${this.label}</div>` : html``}
<div class="selectionBox">
<div class="input-wrapper">
<dees-label .label=${this.label}></dees-label>
<div class="maincontainer" @keydown="${this.isOpened ? this.handleKeyDown : undefined}">
<div class="selectionBox">
${this.enableSearch && !this.opensToTop
? html`
<div class="search top">
@ -247,6 +233,7 @@ export class DeesInputDropdown extends DeesElement {
}}"
>
${this.selectedOption?.option || 'Select...'}
</div>
</div>
</div>
`;
@ -372,4 +359,12 @@ export class DeesInputDropdown extends DeesElement {
event.preventDefault();
}
}
public getValue(): { option: string; key: string; payload?: any } {
return this.selectedOption;
}
public setValue(value: { option: string; key: string; payload?: any }): void {
this.selectedOption = value;
}
}

View File

@ -1,5 +1,6 @@
import {customElement, DeesElement, type TemplateResult, property, html, type CSSResult,} from '@design.estate/dees-element';
import {customElement, type TemplateResult, property, html, css, cssManager} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import { DeesInputBase } from './dees-input-base.js';
declare global {
interface HTMLElementTagNameMap {
@ -8,55 +9,34 @@ declare global {
}
@customElement('dees-input-radio')
export class DeesInputRadio extends DeesElement {
export class DeesInputRadio extends DeesInputBase<DeesInputRadio> {
public static demo = () => html`<dees-input-radio></dees-input-radio>`;
// INSTANCE
public changeSubject = new domtools.plugins.smartrx.rxjs.Subject();
@property({
type: String,
reflect: true,
})
public key: string;
@property()
public label: string = 'Label';
@property()
public value: boolean = false;
@property({
type: Boolean,
})
public required: boolean = false;
@property({
type: Boolean
})
public disabled: boolean = false;
constructor() {
super();
this.labelPosition = 'right'; // Radio buttons default to label on the right
}
public render(): TemplateResult {
return html `
<style>
public static styles = [
...DeesInputBase.baseStyles,
cssManager.defaultStyles,
css`
* {
box-sizing: border-box;
}
:host {
display: block;
position: relative;
margin: 20px 0px;
}
.maincontainer {
transition: all 0.3s;
display: grid;
grid-template-columns: 25px auto;
padding: 5px 0px;
color: #ccc;
}
@ -65,14 +45,6 @@ export class DeesInputRadio extends DeesElement {
color: #fff;
}
.label {
margin-left: 15px;
line-height: 25px;
font-size: 14px;
font-weight: normal;
}
input:focus {
outline: none;
border-bottom: 1px solid #e4002b;
@ -106,12 +78,18 @@ export class DeesInputRadio extends DeesElement {
height: 10px;
border-radius: 10px;
}
</style>
<div class="maincontainer" @click="${this.toggleSelected}">
<div class="checkbox ${this.value ? 'selected' : ''}">
${this.value ? html`<div class="innercircle"></div>`: html``}
`,
];
public render(): TemplateResult {
return html`
<div class="input-wrapper">
<div class="maincontainer" @click="${this.toggleSelected}">
<div class="checkbox ${this.value ? 'selected' : ''}">
${this.value ? html`<div class="innercircle"></div>`: html``}
</div>
</div>
<div class="label">${this.label}</div>
<dees-label .label=${this.label}></dees-label>
</div>
`;
}
@ -124,4 +102,12 @@ export class DeesInputRadio extends DeesElement {
}));
this.changeSubject.next(this);
}
public getValue(): boolean {
return this.value;
}
public setValue(value: boolean): void {
this.value = value;
}
}

View File

@ -1,16 +1,14 @@
import * as colors from './00colors.js';
import { DeesInputBase } from './dees-input-base.js';
import {
customElement,
DeesElement,
type TemplateResult,
property,
html,
cssManager,
css,
type CSSResult,
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
declare global {
interface HTMLElementTagNameMap {
@ -19,47 +17,19 @@ declare global {
}
@customElement('dees-input-text')
export class DeesInputText extends DeesElement {
export class DeesInputText extends DeesInputBase {
public static demo = () => html`
<dees-input-text .label=${'this is a label'} .value=${'test'}></dees-input-text>
<dees-input-text .isPasswordBool=${true}></dees-input-text>
`;
// INSTANCE
public changeSubject = new domtools.plugins.smartrx.rxjs.Subject<DeesInputText>();
@property({
type: String,
})
public label: string;
@property({
type: String,
})
public description: string;
@property({
type: String,
reflect: true,
})
public key: string;
@property({
type: String,
reflect: true,
})
public value: string = '';
@property({
type: Boolean,
})
public required: boolean = false;
@property({
type: Boolean,
})
public disabled: boolean = false;
@property({
type: Boolean,
reflect: true,
@ -87,6 +57,7 @@ export class DeesInputText extends DeesElement {
validationFunction: (value: string) => boolean;
public static styles = [
...DeesInputBase.baseStyles,
cssManager.defaultStyles,
css`
* {
@ -95,9 +66,6 @@ export class DeesInputText extends DeesElement {
:host {
position: relative;
display: grid;
margin: 8px 0px;
margin-bottom: 24px;
z-index: auto;
}
@ -193,33 +161,32 @@ export class DeesInputText extends DeesElement {
}
`}
</style>
<div class="maincontainer">
<div class="input-wrapper">
<dees-label .label=${this.label} .description=${this.description}></dees-label>
<input
type="${this.isPasswordBool && !this.showPasswordBool ? 'password' : 'text'}"
.value=${this.value}
@input="${this.updateValue}"
.disabled=${this.disabled}
/>
<div class="validationContainer">
${this.validationText}
<div class="maincontainer">
<input
type="${this.isPasswordBool && !this.showPasswordBool ? 'password' : 'text'}"
.value=${this.value}
@input="${this.updateValue}"
.disabled=${this.disabled}
/>
<div class="validationContainer">
${this.validationText}
</div>
${this.isPasswordBool
? html`
<div class="showPassword" @click=${this.togglePasswordView}>
<dees-icon .iconFA=${this.showPasswordBool ? 'eye' : 'eyeSlash'}></dees-icon>
</div>
`
: html``}
</div>
${this.isPasswordBool
? html`
<div class="showPassword" @click=${this.togglePasswordView}>
<dees-icon .iconFA=${this.showPasswordBool ? 'eye' : 'eyeSlash'}></dees-icon>
</div>
`
: html``}
</div>
`;
}
firstUpdated() {
const input = this.shadowRoot.querySelector('input');
input.addEventListener('input', (eventArg: InputEvent) => {
});
// Input event handling is already done in updateValue method
}
public async updateValue(eventArg: Event) {
@ -228,16 +195,15 @@ export class DeesInputText extends DeesElement {
this.changeSubject.next(this);
}
public async freeze() {
this.disabled = true;
public getValue(): string {
return this.value;
}
public async unfreeze() {
this.disabled = false;
public setValue(value: string): void {
this.value = value;
}
public async togglePasswordView() {
const domtools = await this.domtoolsPromise;
this.showPasswordBool = !this.showPasswordBool;
console.log(`this.showPasswordBool is: ${this.showPasswordBool}`);
}