Merged PR 1841: feat(ui-input-controls, oms-return-process): introduce text field container,...

feat(ui-input-controls, oms-return-process): introduce text field container, clear, and errors components

- Add `ui-text-field-container`, `ui-text-field-clear`, and `ui-text-field-errors` as standalone components for improved text field composition and error handling.
- Update SCSS to include new styles for container, clear, and errors components, ensuring visual consistency and error highlighting.
- Refactor `ReturnProcessProductQuestionComponent` to use the new containerized text field structure, improving template clarity and error display.
- Update Storybook story for `TextField` to demonstrate new composition and error handling.
- Export new components from the input-controls public API for external usage.

Ref: #4989, #5058
This commit is contained in:
Nino Righi
2025-06-05 17:12:28 +00:00
committed by Lorenz Hilpert
parent f5507a874c
commit b261273228
28 changed files with 597 additions and 91 deletions

View File

@@ -1,24 +1,72 @@
import { type Meta, type StoryObj } from '@storybook/angular';
import { TextFieldComponent } from '@isa/ui/input-controls';
import { moduleMetadata, type Meta, type StoryObj } from '@storybook/angular';
import {
TextFieldClearComponent,
TextFieldComponent,
TextFieldContainerComponent,
TextFieldErrorsComponent,
} from '@isa/ui/input-controls';
const meta: Meta<TextFieldComponent> = {
interface TextFieldStoryProps {
showClear: boolean;
showError1: boolean;
showError2: boolean;
errorText1: string;
errorText2: string;
}
const meta: Meta<TextFieldStoryProps> = {
component: TextFieldComponent,
title: 'ui/input-controls/TextField',
argTypes: {},
argTypes: {
showClear: { control: 'boolean', name: 'Show Clear Button' },
showError1: { control: 'boolean', name: 'Show Error 1' },
showError2: { control: 'boolean', name: 'Show Error 2' },
errorText1: { control: 'text', name: 'Error 1 Text' },
errorText2: { control: 'text', name: 'Error 2 Text' },
},
args: {
showClear: true,
showError1: false,
showError2: false,
errorText1: 'Eingabe ungültig',
errorText2: 'Error Beispiel 2',
},
decorators: [
moduleMetadata({
imports: [
TextFieldClearComponent,
TextFieldContainerComponent,
TextFieldErrorsComponent,
],
}),
],
render: (args) => ({
props: args,
template: `
<ui-text-field-container>
<ui-text-field>
<input type="text" placeholder="Enter your name" />
<input type="text" placeholder="Enter your name" />
<ui-text-field-clear *ngIf="showClear"></ui-text-field-clear>
</ui-text-field>
<ui-text-field-errors>
<span *ngIf="showError1">{{ errorText1 }}</span>
<span *ngIf="showError2">{{ errorText2 }}</span>
</ui-text-field-errors>
</ui-text-field-container>
`,
}),
};
export default meta;
type Story = StoryObj<TextFieldComponent>;
type Story = StoryObj<TextFieldStoryProps>;
export const Default: Story = {
args: {},
args: {
showClear: true,
showError1: false,
showError2: false,
errorText1: 'Eingabe ungültig',
errorText2: 'Error Beispiel 2',
},
};

View File

@@ -1 +1,3 @@
export * from './lib/scanner.service';
export * from './lib/render-if-scanner-is-ready.directive';
export * from './lib/errors';

View File

@@ -0,0 +1 @@
export * from './platform-not-supported.error';

View File

@@ -0,0 +1,6 @@
export class PlatformNotSupportedError extends Error {
constructor() {
super('ScannerService is only supported on iOS and Android platforms');
this.name = 'PlatformNotSupportedError';
}
}

View File

@@ -0,0 +1,75 @@
import { signal, Signal, WritableSignal } from '@angular/core';
import {
createDirectiveFactory,
SpectatorDirective,
} from '@ngneat/spectator/jest';
import { ScannerReadyDirective } from './render-if-scanner-is-ready.directive';
import { ScannerService } from './scanner.service';
// FULLY MOCK EXTERNAL DEPENDENCIES
jest.mock('scandit-web-datacapture-core', () => ({
configure: jest.fn(),
}));
jest.mock('scandit-web-datacapture-barcode', () => ({
barcodeCaptureLoader: jest.fn(() => 'mockedBarcodeLoader'),
}));
const loggerInfo = jest.fn();
const loggerError = jest.fn();
jest.mock('@isa/core/logging', () => ({
logger: jest.fn(() => ({ info: loggerInfo, error: loggerError })),
}));
describe('ScannerReadyDirective', () => {
let spectator: SpectatorDirective<ScannerReadyDirective>;
let readySignal: WritableSignal<boolean>;
let scannerServiceMock: jest.Mocked<ScannerService>;
const createDirective = createDirectiveFactory({
directive: ScannerReadyDirective,
mocks: [ScannerService], // mock out the real ScannerService (and its Config deps)
detectChanges: false, // manual CD
template: `<div *sharedScannerReady>Ready</div>`,
});
beforeEach(() => {
// 1) instantiate
spectator = createDirective();
// 2) grab the Spectator mock
scannerServiceMock = spectator.inject(ScannerService);
// 3) override its `ready` with a real Angular signal
readySignal = signal(false);
(scannerServiceMock as any).ready = readySignal;
});
it('should render when ready=true', () => {
// ARRANGE
readySignal.set(true);
// ACT
spectator.detectChanges();
// ASSERT
expect(spectator.query('div')).toHaveText('Ready');
expect(spectator.query('div')).toBeTruthy();
});
it('should not render when ready=false', () => {
// ARRANGE
readySignal.set(false);
// ACT
spectator.detectChanges();
// ASSERT
expect(spectator.query('div')).not.toExist();
});
it('should update view when ready flips', () => {
// ARRANGE
readySignal.set(false);
spectator.detectChanges();
expect(spectator.query('div')).not.toExist();
// ACT
readySignal.set(true);
spectator.detectChanges();
// ASSERT
expect(spectator.query('div')).toHaveText('Ready');
});
});

View File

@@ -0,0 +1,39 @@
import {
Directive,
TemplateRef,
ViewContainerRef,
inject,
effect,
input,
} from '@angular/core';
import { ScannerService } from './scanner.service';
@Directive({
selector: '[sharedScannerReady]',
standalone: true,
})
export class ScannerReadyDirective {
#scanner = inject(ScannerService);
#vcRef = inject(ViewContainerRef);
#tpl = inject(TemplateRef<unknown>);
scannerReadyElse = input<TemplateRef<unknown> | undefined>();
constructor() {
effect(() => {
this.#updateView();
});
}
#updateView(): void {
this.#vcRef.clear();
if (this.#scanner.ready()) {
this.#vcRef.createEmbeddedView(this.#tpl);
return;
}
const elseTpl = this.scannerReadyElse();
if (elseTpl) {
this.#vcRef.createEmbeddedView(elseTpl);
}
}
}

View File

@@ -0,0 +1,141 @@
import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest';
import {
ScannerService,
SCANDIT_LICENSE,
SCANDIT_LIBRARY_LOCATION,
} from './scanner.service';
import { Platform } from '@angular/cdk/platform';
import { configure } from 'scandit-web-datacapture-core';
import { Config, CONFIG_DATA } from '@isa/core/config'; // <-- Use the real tokens
class MockConfig {
get(key: string) {
if (key === 'licence.scandit') return 'FAKE-LICENSE';
return undefined;
}
}
jest.mock('scandit-web-datacapture-core', () => ({
configure: jest.fn(),
}));
jest.mock('scandit-web-datacapture-barcode', () => ({
barcodeCaptureLoader: jest.fn(() => 'mockedBarcodeLoader'),
}));
const loggerInfo = jest.fn();
const loggerError = jest.fn();
jest.mock('@isa/core/logging', () => ({
logger: jest.fn(() => ({
info: loggerInfo,
error: loggerError,
})),
}));
describe('ScannerService', () => {
let spectator: SpectatorService<ScannerService>;
const createService = createServiceFactory({
service: ScannerService,
providers: [
{ provide: SCANDIT_LICENSE, useValue: 'FAKE-LICENSE' },
{ provide: SCANDIT_LIBRARY_LOCATION, useValue: 'https://fake-location/' },
{ provide: Platform, useValue: { IOS: true, ANDROID: false } },
{ provide: CONFIG_DATA, useValue: {} },
{ provide: Config, useClass: MockConfig },
],
});
beforeEach(() => {
jest.clearAllMocks();
spectator = createService();
});
describe('configure', () => {
it('should configure successfully on supported platform', async () => {
(configure as jest.Mock).mockResolvedValue(undefined);
await spectator.service.configure();
expect(configure).toHaveBeenCalledWith({
licenseKey: 'FAKE-LICENSE',
libraryLocation: 'https://fake-location/',
moduleLoaders: ['mockedBarcodeLoader'],
});
expect(loggerInfo).toHaveBeenCalledWith('Scanner initializing');
expect(loggerInfo).toHaveBeenCalledWith('Scanner ready');
});
it('should not re-configure if already initializing or ready', async () => {
(configure as jest.Mock).mockResolvedValue(undefined);
await spectator.service.configure();
await spectator.service.configure();
expect((configure as jest.Mock).mock.calls.length).toBe(1);
});
it('should throw and log error if configure throws', async () => {
(configure as jest.Mock).mockRejectedValue(new Error('Config failed'));
await expect(spectator.service.configure()).rejects.toThrow(
'Config failed',
);
expect(loggerError).toHaveBeenCalledWith(
'Failed to configure Scandit',
expect.any(Error),
);
});
});
describe('ready', () => {
it('should return true if status is Ready', async () => {
(configure as jest.Mock).mockResolvedValue(undefined);
await spectator.service.configure();
expect(spectator.service.ready()).toBe(true);
});
it('should return false if status is not Ready', () => {
expect(spectator.service.ready()).toBe(false);
});
});
describe('open', () => {
it('should call configure and resolve undefined', async () => {
(configure as jest.Mock).mockResolvedValue(undefined);
const result = await spectator.service.open();
expect(configure).toHaveBeenCalled();
expect(loggerInfo).toHaveBeenCalledWith('Scanner not implemented');
expect(result).toBeUndefined();
});
it('should propagate error from configure', async () => {
(configure as jest.Mock).mockRejectedValue(new Error('Config failed'));
await expect(spectator.service.open()).rejects.toThrow('Config failed');
});
});
// Additional tests for edge cases and error handling
it('should set status to Error if configure throws', async () => {
(configure as jest.Mock).mockRejectedValue(new Error('fail'));
try {
await spectator.service.configure();
} catch {
// ignore
}
expect(spectator.service.ready()).toBe(false);
expect(loggerError).toHaveBeenCalledWith(
'Failed to configure Scandit',
expect.any(Error),
);
});
it('should call logger.info with "Scanner not implemented" when open is called', async () => {
(configure as jest.Mock).mockResolvedValue(undefined);
await spectator.service.open();
expect(loggerInfo).toHaveBeenCalledWith('Scanner not implemented');
});
});

View File

@@ -1,10 +1,18 @@
import { inject, Injectable, InjectionToken } from '@angular/core';
import {
computed,
inject,
Injectable,
InjectionToken,
signal,
untracked,
} from '@angular/core';
import { Config } from '@isa/core/config';
import { z } from 'zod';
import { configure } from 'scandit-web-datacapture-core';
import { barcodeCaptureLoader, Symbology } from 'scandit-web-datacapture-barcode';
export { Symbology };
import { barcodeCaptureLoader } from 'scandit-web-datacapture-barcode';
import { Platform } from '@angular/cdk/platform';
import { PlatformNotSupportedError } from './errors/platform-not-supported.error';
import { logger } from '@isa/core/logging';
export const SCANDIT_LICENSE = new InjectionToken<string>('ScanditLicense', {
factory() {
@@ -12,39 +20,83 @@ export const SCANDIT_LICENSE = new InjectionToken<string>('ScanditLicense', {
},
});
export const SCANDIT_LIBRARY_LOCATION = new InjectionToken<string>('ScanditLibraryLocation', {
factory() {
return new URL('scandit', document.baseURI).toString();
export const SCANDIT_LIBRARY_LOCATION = new InjectionToken<string>(
'ScanditLibraryLocation',
{
factory() {
return new URL('scandit', document.baseURI).toString();
},
},
});
);
export const ScannerStatus = {
None: 'none',
Initializing: 'initializing',
Ready: 'ready',
Error: 'error',
} as const;
export type ScannerStatus = (typeof ScannerStatus)[keyof typeof ScannerStatus];
@Injectable({ providedIn: 'root' })
export class ScannerService {
readonly licenseKey = inject(SCANDIT_LICENSE);
#logger = logger(() => ({ service: 'ScannerService' }));
#licenseKey = inject(SCANDIT_LICENSE);
#platform = inject(Platform);
#libraryLocation = inject(SCANDIT_LIBRARY_LOCATION);
readonly libraryLocation = inject(SCANDIT_LIBRARY_LOCATION);
#status = signal<ScannerStatus>(ScannerStatus.None);
private configured = false;
ready = computed(() => {
untracked(() => {
this.configure();
});
return this.#status() === ScannerStatus.Ready;
});
async configure() {
if (this.configured) {
private checkPlatformSupported() {
if (this.#platform.IOS || this.#platform.ANDROID) {
return;
}
await configure({
licenseKey: this.licenseKey,
libraryLocation: this.libraryLocation,
moduleLoaders: [barcodeCaptureLoader()],
});
this.configured = true;
throw new PlatformNotSupportedError();
}
async open(args: { symbologies: Symbology[] }): Promise<string | undefined> {
async configure() {
const status = this.#status();
if (
status === ScannerStatus.Initializing ||
status === ScannerStatus.Ready
) {
return;
}
this.#status.set(ScannerStatus.Initializing);
this.#logger.info('Scanner initializing');
try {
this.checkPlatformSupported();
await configure({
licenseKey: this.#licenseKey,
libraryLocation: this.#libraryLocation,
moduleLoaders: [barcodeCaptureLoader()],
});
} catch (error) {
this.#status.set(ScannerStatus.Error);
this.#logger.error('Failed to configure Scandit', error);
throw error;
}
this.#status.set(ScannerStatus.Ready);
this.#logger.info('Scanner ready');
}
async open(): Promise<string | undefined> {
await this.configure();
return new Promise((resolve) => {
console.warn('Scanner not implemented');
this.#logger.info('Scanner not implemented');
setTimeout(() => {
resolve(undefined);
}, 1000);

View File

@@ -1,9 +1,11 @@
@let p = product();
<div class="flex gap-6 flex-shrink-0 justify-between items-center">
<div>
{{ question().description }}
</div>
<div class="flex flex-col gap-6">
<div class="text-right">
<div class="flex gap-4 items-center">
<ui-text-field-container>
<ui-text-field size="small" class="min-w-[22.875rem]">
<input
uiInputControl
@@ -11,36 +13,47 @@
placeholder="EAN eingeben / scannen"
type="text"
[formControl]="control"
(cleared)="resetProduct()"
/>
<button
class="px-0"
uiTextButton
size="small"
color="strong"
(click)="check()"
[pending]="status().fetching"
>
Prüfen
</button>
@if (!p) {
<button
class="px-0"
uiTextButton
size="small"
color="strong"
(click)="check()"
[pending]="status().fetching"
>
Prüfen
</button>
} @else {
<ui-text-field-clear></ui-text-field-clear>
}
</ui-text-field>
</div>
<ui-text-field-errors>
@if (!p && status().hasResult === false) {
<span>Kein Artikel gefunden</span>
}
@if (control.invalid && control.touched && !control.dirty) {
<span>Die eingegebene EAN ist ungültig</span>
}
</ui-text-field-errors>
</ui-text-field-container>
<ui-icon-button
*sharedScannerReady
class="self-start"
type="button"
[color]="'primary'"
[disabled]="!!control?.value"
(click)="check()"
name="isaActionScanner"
></ui-icon-button>
</div>
</div>
@let p = product();
@if (!p && status().hasResult === false) {
<div class="text-isa-accent-red isa-text-body-2-bold self-end">
Kein Artikel gefunden
</div>
}
@if (control.invalid && control.touched && !control.dirty) {
<div class="text-isa-accent-red isa-text-body-2-bold self-end">
Die eingegebene EAN ist ungültig
</div>
}
@if (p) {
<div class="mb-6 flex flex-col gap-4">
<div class="isa-text-body-2-bold">Gelieferter Artikel:</div>

View File

@@ -19,16 +19,22 @@ import {
ReturnProcessProductQuestion,
ReturnProcessStore,
} from '@isa/oms/data-access';
import { TextButtonComponent } from '@isa/ui/buttons';
import { IconButtonComponent, TextButtonComponent } from '@isa/ui/buttons';
import {
InputControlDirective,
TextFieldClearComponent,
TextFieldComponent,
TextFieldContainerComponent,
TextFieldErrorsComponent,
} from '@isa/ui/input-controls';
import { rxMethod } from '@ngrx/signals/rxjs-interop';
import { filter, pipe, switchMap, tap } from 'rxjs';
import { CatalougeSearchService } from '@isa/catalogue/data-access';
import { tapResponse } from '@ngrx/operators';
import { ProductImageDirective } from '@isa/shared/product-image';
import { provideIcons } from '@ng-icons/core';
import { isaActionScanner } from '@isa/icons';
import { ScannerReadyDirective } from '@isa/core/scanner';
const eanValidator: ValidatorFn = (
control: AbstractControl,
@@ -47,11 +53,17 @@ const eanValidator: ValidatorFn = (
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
ReactiveFormsModule,
TextFieldComponent,
InputControlDirective,
TextButtonComponent,
TextFieldContainerComponent,
TextFieldErrorsComponent,
TextFieldComponent,
TextFieldClearComponent,
ProductImageDirective,
IconButtonComponent,
ScannerReadyDirective,
],
providers: [provideIcons({ isaActionScanner })],
})
export class ReturnProcessProductQuestionComponent {
#returnProcessStore = inject(ReturnProcessStore);
@@ -81,6 +93,14 @@ export class ReturnProcessProductQuestionComponent {
});
}
resetProduct() {
this.product.set(undefined);
this.#returnProcessStore.removeAnswer(
this.returnProcessId(),
this.question().key,
);
}
check = rxMethod<void>(
pipe(
filter(() => this.control.valid),
@@ -101,11 +121,7 @@ export class ReturnProcessProductQuestionComponent {
);
this.status.set({ fetching: false, hasResult: undefined });
} else {
this.product.set(undefined);
this.#returnProcessStore.removeAnswer(
this.returnProcessId(),
this.question().key,
);
this.resetProduct();
this.status.set({ fetching: false, hasResult: false });
}
},

View File

@@ -32,14 +32,13 @@
}
</ui-search-bar>
@if (canScan) {
<ui-icon-button
class="desktop:invisible"
type="submit"
[color]="'primary'"
[disabled]="control.invalid"
(click)="triggerSearch.emit()"
name="isaActionScanner"
></ui-icon-button>
}
<ui-icon-button
*sharedScannerReady
class="desktop:invisible"
type="submit"
[color]="'primary'"
[disabled]="control.invalid"
(click)="triggerSearch.emit()"
name="isaActionScanner"
></ui-icon-button>
}

View File

@@ -15,12 +15,12 @@ import {
} from '@isa/ui/search-bar';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { IconButtonColor, IconButtonComponent } from '@isa/ui/buttons';
import { NgIconComponent, provideIcons } from '@ng-icons/core';
import { provideIcons } from '@ng-icons/core';
import { isaActionSearch, isaActionScanner } from '@isa/icons';
import { FilterService, TextFilterInput } from '../../core';
import { InputType } from '../../types';
import { Platform } from '@angular/cdk/platform';
import { toSignal } from '@angular/core/rxjs-interop';
import { ScannerReadyDirective } from '@isa/core/scanner';
@Component({
selector: 'filter-search-bar-input',
@@ -32,9 +32,9 @@ import { toSignal } from '@angular/core/rxjs-interop';
imports: [
UiSearchBarComponent,
IconButtonComponent,
NgIconComponent,
ReactiveFormsModule,
UiSearchBarClearComponent,
ScannerReadyDirective,
],
host: {
'[class]': "['filter-search-bar-input', appearanceClass()]",
@@ -43,7 +43,6 @@ import { toSignal } from '@angular/core/rxjs-interop';
})
export class SearchBarInputComponent {
readonly filterService = inject(FilterService);
#platform = inject(Platform);
control = new FormControl();
valueChanges = toSignal(this.control.valueChanges);
@@ -52,8 +51,6 @@ export class SearchBarInputComponent {
buttonColor = input<IconButtonColor>('brand');
appearance = input<'main' | 'results'>('main');
canScan = this.#platform.ANDROID || this.#platform.IOS;
appearanceClass = computed(
() => `filter-search-bar-input__${this.appearance()}`,
);

View File

@@ -4,6 +4,15 @@
align-items: center;
flex-shrink: 0;
@apply rounded-full cursor-pointer bg-isa-white;
&.disabled {
@apply pointer-events-none; // Da es kein natives Button Element ist, müssen hier Pointer-Events deaktiviert werden
}
}
.ui-icon-button__small {
height: 1.75rem;
width: 1.75rem;
}
.ui-icon-button__medium {

View File

@@ -79,6 +79,7 @@ export class IconButtonComponent {
iconSize = computed(() => {
const size = this.size();
if (size === 'small') return `1rem`;
if (size === 'medium') return `1.25rem`;
if (size === 'large') return `1.5rem`;

View File

@@ -30,6 +30,7 @@ export type TextButtonColor =
(typeof TextButtonColor)[keyof typeof TextButtonColor];
export const IconButtonSize = {
Small: 'small',
Medium: 'medium',
Large: 'large',
} as const;

View File

@@ -7,7 +7,10 @@ export * from './lib/dropdown/dropdown.component';
export * from './lib/dropdown/dropdown.types';
export * from './lib/listbox/listbox-item.directive';
export * from './lib/listbox/listbox.directive';
export * from './lib/text-field/text-field.component';
export * from './lib/text-field/textarea.component';
export * from './lib/text-field/text-field.component';
export * from './lib/text-field/text-field-clear.component';
export * from './lib/text-field/text-field-container.component';
export * from './lib/text-field/text-field-errors.component';
export * from './lib/chips/chips.component';
export * from './lib/chips/chip-option.component';

View File

@@ -1,7 +1,10 @@
@use "./lib/checkbox/checkbox";
@use "./lib/checkbox/checklist";
@use "./lib/chips/chips";
@use "./lib/dropdown/dropdown";
@use "./lib/listbox/listbox";
@use "./lib/text-field/text-field";
@use "./lib/text-field/textarea";
@use './lib/checkbox/checkbox';
@use './lib/checkbox/checklist';
@use './lib/chips/chips';
@use './lib/dropdown/dropdown';
@use './lib/listbox/listbox';
@use './lib/text-field/text-field';
@use './lib/text-field/text-field-clear';
@use './lib/text-field/text-field-container';
@use './lib/text-field/text-field-errors';
@use './lib/text-field/textarea';

View File

@@ -1,11 +1,15 @@
import { Directive, inject, input, OnInit } from '@angular/core';
import { Directive, inject, model, output } from '@angular/core';
import { NgControl } from '@angular/forms';
@Directive({ selector: 'input[uiInputControl]', host: { class: 'ui-input-control' } })
@Directive({
selector: 'input[uiInputControl]',
host: { class: 'ui-input-control' },
})
export class InputControlDirective<T> {
readonly control = inject(NgControl, { optional: true, self: true });
readonly value = input<T>();
readonly value = model<T>();
cleared = output<void>();
getValue(): T | undefined {
if (this.control) {
@@ -14,4 +18,13 @@ export class InputControlDirective<T> {
return this.value();
}
clear(): void {
if (this.control) {
this.control.reset();
} else {
this.value.set(undefined);
}
this.cleared.emit();
}
}

View File

@@ -0,0 +1,5 @@
.ui-text-field-clear {
ui-icon-button {
@apply text-isa-neutral-900;
}
}

View File

@@ -0,0 +1,9 @@
.ui-text-field-container {
@apply flex flex-col gap-1 items-start justify-center;
&:has(.ui-text-field-errors > *) {
.ui-text-field {
@apply border-isa-accent-red;
}
}
}

View File

@@ -0,0 +1,3 @@
.ui-text-field-errors {
@apply flex flex-col text-isa-accent-red isa-text-caption-regular;
}

View File

@@ -0,0 +1,7 @@
<ui-icon-button
type="button"
[color]="'neutral'"
[size]="size()"
(click)="hostComponent.inputControl().clear()"
name="isaActionClose"
></ui-icon-button>

View File

@@ -0,0 +1,31 @@
import {
ChangeDetectionStrategy,
Component,
computed,
inject,
} from '@angular/core';
import { TextFieldComponent } from './text-field.component';
import { provideIcons } from '@ng-icons/core';
import { isaActionClose } from '@isa/icons';
import { IconButtonComponent } from '@isa/ui/buttons';
@Component({
selector: 'ui-text-field-clear',
templateUrl: './text-field-clear.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
host: {
'[class]': '["ui-text-field-clear", sizeClass()]',
},
providers: [provideIcons({ isaActionClose })],
imports: [IconButtonComponent],
})
export class TextFieldClearComponent {
hostComponent = inject(TextFieldComponent, { host: true });
size = this.hostComponent.size;
sizeClass = computed(() => {
return `ui-text-field-clear__${this.size()}`;
});
}

View File

@@ -0,0 +1 @@
<ng-content></ng-content>

View File

@@ -0,0 +1,12 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
selector: 'ui-text-field-container',
templateUrl: './text-field-container.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
host: {
'[class]': '["ui-text-field-container"]',
},
})
export class TextFieldContainerComponent {}

View File

@@ -0,0 +1 @@
<ng-content></ng-content>

View File

@@ -0,0 +1,12 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
selector: 'ui-text-field-errors',
templateUrl: './text-field-errors.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
host: {
'[class]': '["ui-text-field-errors"]',
},
})
export class TextFieldErrorsComponent {}

View File

@@ -1,4 +1,10 @@
import { ChangeDetectionStrategy, Component, computed, contentChild, input } from '@angular/core';
import {
ChangeDetectionStrategy,
Component,
computed,
contentChild,
input,
} from '@angular/core';
import { InputControlDirective } from '../core/input-control.directive';
export const TextFieldSize = {