feat(web): campos de formulario SBO reutilizables
- sbo-field: fila etiqueta + control, con obligatorio (*), flecha dorada, etiqueta enlazable y mensaje de error. - sbo-text-field, sbo-textarea-field, sbo-number-field, sbo-date-field, sbo-select-field y sbo-lookup-field, con enlace bidireccional por señales ([(value)]) y entradas comunes (label, required, readonly, disabled, error). - sbo-number-field: formateado fuera de foco (1,234.50), acepta 12,5 y 1,234.50 al escribir, redondea a los decimales indicados y conserva el valor si el texto no es un número (parseDecimal). - sbo-select-field devuelve el valor con su tipo original (número o texto). - sbo-lookup-field: botón "…" y F4 abren sbo-value-help (VhLookup); elegir rellena el campo, emite la fila y devuelve el foco; escribir el código a mano sigue siendo válido. - Estilos globales de formulario (forms.css) tomados del mockup, con ancho de etiqueta configurable (--label-w). - Verificado en Chrome contra SAP real, en claro y oscuro: cliente elegido de la lista rellena código, nombre y RUC; edición de números; selects. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3d5a81e806
commit
aa2c483723
16 files changed
+917
-1
No files matched your search
@@ -0,0 +1,102 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
type ElementRef,
|
||||
booleanAttribute,
|
||||
input,
|
||||
model,
|
||||
output,
|
||||
signal,
|
||||
viewChild,
|
||||
} from '@angular/core';
|
||||
import { ValueHelp } from '../value-help/value-help';
|
||||
import type { VhLookup, VhRow } from '../value-help/value-help.types';
|
||||
import { FieldBase } from './field-base';
|
||||
import { SboField } from './sbo-field';
|
||||
|
||||
/**
|
||||
* Campo con botón "…" que abre una lista de valores (flecha dorada de SAP).
|
||||
* Escribir el código a mano también es válido; elegir de la lista lo rellena y emite `picked`.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'sbo-lookup-field',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [SboField, ValueHelp],
|
||||
template: `
|
||||
<sbo-field
|
||||
[label]="label()"
|
||||
[for]="id"
|
||||
[required]="required()"
|
||||
[error]="error()"
|
||||
action
|
||||
[linkable]="linkable()"
|
||||
(labelClick)="linkClick.emit()"
|
||||
>
|
||||
<div class="sbo-pick">
|
||||
<input
|
||||
#box
|
||||
class="sbo-in"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
[id]="id"
|
||||
[value]="value()"
|
||||
[readOnly]="readOnly()"
|
||||
[disabled]="disabled()"
|
||||
[required]="required()"
|
||||
[attr.aria-invalid]="error() ? 'true' : null"
|
||||
(input)="value.set($any($event.target).value)"
|
||||
(keydown.f4)="$event.preventDefault(); openList()"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
title="Lista de valores (F4)"
|
||||
[attr.aria-label]="'Abrir la lista de ' + label()"
|
||||
[disabled]="readOnly() || disabled()"
|
||||
(click)="openList()"
|
||||
>
|
||||
…
|
||||
</button>
|
||||
</div>
|
||||
</sbo-field>
|
||||
<sbo-value-help
|
||||
[open]="listOpen()"
|
||||
[title]="lookup().title"
|
||||
[resource]="lookup().resource"
|
||||
[columns]="lookup().columns"
|
||||
[baseFilter]="lookup().baseFilter"
|
||||
[orderby]="lookup().orderby"
|
||||
(selected)="onPicked($event)"
|
||||
(closed)="closeList()"
|
||||
/>
|
||||
`,
|
||||
styles: ':host { display: block; }',
|
||||
})
|
||||
export class LookupField extends FieldBase {
|
||||
readonly value = model('');
|
||||
readonly lookup = input.required<VhLookup>();
|
||||
/** La etiqueta abre el objeto enlazado (evento `linkClick`). */
|
||||
readonly linkable = input(false, { transform: booleanAttribute });
|
||||
|
||||
readonly picked = output<VhRow>();
|
||||
readonly linkClick = output<void>();
|
||||
|
||||
protected readonly listOpen = signal(false);
|
||||
private readonly box = viewChild.required<ElementRef<HTMLInputElement>>('box');
|
||||
|
||||
protected openList(): void {
|
||||
if (!this.readOnly() && !this.disabled()) this.listOpen.set(true);
|
||||
}
|
||||
|
||||
protected closeList(): void {
|
||||
this.listOpen.set(false);
|
||||
this.box().nativeElement.focus();
|
||||
}
|
||||
|
||||
protected onPicked(rows: VhRow[]): void {
|
||||
const row = rows[0];
|
||||
if (!row) return;
|
||||
const key = this.lookup().valueKey ?? this.lookup().columns[0]?.key ?? '';
|
||||
this.value.set(String(row[key] ?? ''));
|
||||
this.picked.emit(row);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user