diff --git a/README.md b/README.md index 08305672..36bf223e 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ The versioning of material-addons is based on the Angular version. The Angular v _Hint: Changes marked as **visible change** directly affect your application during version upgrade. **Breaking** requires your attention during upgrade._ +- **19.0.3**: Fix ReadonlyFormField/Wrapper: fix performance issues - **19.0.2**: bugfix for version: fix dist output - **19.0.0**: Upgrade to Angular 19 - **18.0.5**: FileUpload: Added additional param as 'removable' which allows user to remove file from fileList [#212](https://github.com/porscheinformatik/material-addons/pull/212) diff --git a/projects/material-addons/package.json b/projects/material-addons/package.json index 0085d64c..8f4cbb11 100644 --- a/projects/material-addons/package.json +++ b/projects/material-addons/package.json @@ -1,6 +1,6 @@ { "name": "@porscheinformatik/material-addons", - "version": "19.0.1", + "version": "19.0.3", "description": "Custom theme and components for Angular Material", "homepage": "https://github.com/porscheinformatik/material-addons", "repository": { diff --git a/projects/material-addons/src/lib/alert/alert.component.scss b/projects/material-addons/src/lib/alert/alert.component.scss index 97499395..98309ee7 100644 --- a/projects/material-addons/src/lib/alert/alert.component.scss +++ b/projects/material-addons/src/lib/alert/alert.component.scss @@ -26,6 +26,7 @@ .alert .icon { margin-right: 8px; + margin-top: 4px; } .alert .message { @@ -35,7 +36,6 @@ .alert .close-btn { color: inherit; margin-left: auto; - padding: 0; } .alert .action-btn { diff --git a/projects/material-addons/src/lib/readonly/readonly-form-field-wrapper/readonly-form-field-wrapper.component.html b/projects/material-addons/src/lib/readonly/readonly-form-field-wrapper/readonly-form-field-wrapper.component.html index fb279c6c..3c5ef812 100644 --- a/projects/material-addons/src/lib/readonly/readonly-form-field-wrapper/readonly-form-field-wrapper.component.html +++ b/projects/material-addons/src/lib/readonly/readonly-form-field-wrapper/readonly-form-field-wrapper.component.html @@ -1,4 +1,4 @@ -
+
@@ -10,7 +10,6 @@ [textAlign]="textAlign" [formatNumber]="formatNumber" [decimalPlaces]="decimalPlaces" - [roundDisplayValue]="roundValue" [autofillDecimals]="autofillDecimals" [unit]="unit" [unitPosition]="unitPosition" diff --git a/projects/material-addons/src/lib/readonly/readonly-form-field-wrapper/readonly-form-field-wrapper.component.css b/projects/material-addons/src/lib/readonly/readonly-form-field-wrapper/readonly-form-field-wrapper.component.scss similarity index 100% rename from projects/material-addons/src/lib/readonly/readonly-form-field-wrapper/readonly-form-field-wrapper.component.css rename to projects/material-addons/src/lib/readonly/readonly-form-field-wrapper/readonly-form-field-wrapper.component.scss diff --git a/projects/material-addons/src/lib/readonly/readonly-form-field-wrapper/readonly-form-field-wrapper.component.spec.ts b/projects/material-addons/src/lib/readonly/readonly-form-field-wrapper/readonly-form-field-wrapper.component.spec.ts new file mode 100644 index 00000000..d95d20a7 --- /dev/null +++ b/projects/material-addons/src/lib/readonly/readonly-form-field-wrapper/readonly-form-field-wrapper.component.spec.ts @@ -0,0 +1,81 @@ +import { Component, DebugElement } from '@angular/core'; +import { ReadOnlyFormFieldWrapperComponent } from './readonly-form-field-wrapper.component'; +import { ReadOnlyFormFieldComponent } from '../readonly-form-field/readonly-form-field.component'; +import { FormControl, FormGroup, FormGroupDirective, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { ReadOnlyFormFieldModule } from '../readonly-form-field.module'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInput } from '@angular/material/input'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; + +@Component({ + template: ` +
+ + + Test Label + + + +
+ `, + standalone: true, + imports: [ReadOnlyFormFieldModule, MatFormFieldModule, FormsModule, ReactiveFormsModule, MatInput], +}) +class WrapperTestHostComponent { + readonly = true; + value = 'Test Value'; + form = new FormGroup({ test: new FormControl('Initial Value') }); +} + +describe('ReadOnlyFormFieldWrapperComponent', () => { + let fixture: ComponentFixture; + let hostComponent: WrapperTestHostComponent; + let wrapperDebugEl: DebugElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [WrapperTestHostComponent, NoopAnimationsModule], + providers: [FormGroupDirective], + }).compileComponents(); + + fixture = TestBed.createComponent(WrapperTestHostComponent); + hostComponent = fixture.componentInstance; + wrapperDebugEl = fixture.debugElement; + fixture.detectChanges(); + }); + + it('should create the host and wrapper component', () => { + expect(hostComponent).toBeTruthy(); + const wrapper = wrapperDebugEl.query(By.directive(ReadOnlyFormFieldWrapperComponent)); + expect(wrapper).toBeTruthy(); + }); + + it('should render readonly component when readonly is true', () => { + const readonlyField = wrapperDebugEl.query(By.directive(ReadOnlyFormFieldComponent)); + expect(readonlyField).toBeTruthy(); + }); + + it('should render projected content when readonly is false', () => { + hostComponent.readonly = false; + fixture.detectChanges(); + const input = wrapperDebugEl.query(By.css('input')); + expect(input).toBeTruthy(); + expect(input.nativeElement.value).toBe('Initial Value'); + }); + + it('should emit suffixClickedEmitter', () => { + const wrapperInstance = wrapperDebugEl.query(By.directive(ReadOnlyFormFieldWrapperComponent)).componentInstance; + jest.spyOn(wrapperInstance.suffixClickedEmitter, 'emit'); + wrapperInstance.suffixClicked(); + expect(wrapperInstance.suffixClickedEmitter.emit).toHaveBeenCalled(); + }); + + it('should emit prefixClickedEmitter', () => { + const wrapperInstance = wrapperDebugEl.query(By.directive(ReadOnlyFormFieldWrapperComponent)).componentInstance; + jest.spyOn(wrapperInstance.prefixClickedEmitter, 'emit'); + wrapperInstance.prefixClicked(); + expect(wrapperInstance.prefixClickedEmitter.emit).toHaveBeenCalled(); + }); +}); diff --git a/projects/material-addons/src/lib/readonly/readonly-form-field-wrapper/readonly-form-field-wrapper.component.ts b/projects/material-addons/src/lib/readonly/readonly-form-field-wrapper/readonly-form-field-wrapper.component.ts index c3555802..0a681c91 100644 --- a/projects/material-addons/src/lib/readonly/readonly-form-field-wrapper/readonly-form-field-wrapper.component.ts +++ b/projects/material-addons/src/lib/readonly/readonly-form-field-wrapper/readonly-form-field-wrapper.component.ts @@ -1,13 +1,12 @@ import { - AfterViewChecked, AfterViewInit, + ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, Input, OnChanges, - OnInit, Output, SimpleChanges, ViewChild, @@ -25,15 +24,16 @@ import { ObserversModule } from '@angular/cdk/observers'; @Component({ selector: 'mad-readonly-form-field-wrapper', templateUrl: './readonly-form-field-wrapper.component.html', - styleUrls: ['./readonly-form-field-wrapper.component.css'], + styleUrls: ['./readonly-form-field-wrapper.component.scss'], viewProviders: [{ provide: ControlContainer, useExisting: FormGroupDirective }], imports: [NgIf, ReadOnlyFormFieldComponent, ObserversModule], + changeDetection: ChangeDetectionStrategy.OnPush, }) -export class ReadOnlyFormFieldWrapperComponent implements OnInit, AfterViewInit, OnChanges, AfterViewChecked { +export class ReadOnlyFormFieldWrapperComponent implements AfterViewInit, OnChanges { @ViewChild('contentWrapper', { static: false }) - originalContent: ElementRef; + originalContent!: ElementRef; @ViewChild('readOnlyContentWrapper', { static: false }) - readOnlyContentWrapper: ElementRef; + readOnlyContentWrapper!: ElementRef; /** * If set to "false", the contained mat-form-field is rendered in all it's glory. @@ -55,7 +55,7 @@ export class ReadOnlyFormFieldWrapperComponent implements OnInit, AfterViewInit, @Input('unit') unit: string | null = null; @Input('unitPosition') unitPosition: 'right' | 'left' = 'left'; @Input('errorMessage') errorMessage: string | null = null; - @Input() id: string; + @Input() id!: string; /** * If set to "false", a readonly input will be rendered. * If set to "true", a readonly textarea will be rendered instead. @@ -65,7 +65,7 @@ export class ReadOnlyFormFieldWrapperComponent implements OnInit, AfterViewInit, /** * Defines the rows for the readonly textarea. */ - @Input() rows: number; + @Input() rows!: number; /** * If shrinkIfEmpty is set to "false", nothing changes @@ -74,15 +74,14 @@ export class ReadOnlyFormFieldWrapperComponent implements OnInit, AfterViewInit, * Otherwise, the defined rows-value will be used */ @Input() shrinkIfEmpty = false; - @Input() hideIconInReadOnlyMode = false; /** * suffix iocon */ - @Input() suffix: string; + @Input() suffix!: string; /** * prefix iocon */ - @Input() prefix: string; + @Input() prefix!: string; /** * if cdkTextareaAutosize is active for textareas */ @@ -93,28 +92,27 @@ export class ReadOnlyFormFieldWrapperComponent implements OnInit, AfterViewInit, /** * Automatically taken from the contained */ - label: string; + label!: string; + private initialized = false; constructor( private changeDetector: ChangeDetectorRef, private rootFormGroup: FormGroupDirective, ) {} - ngOnInit(): void { - this.doRendering(); + ngOnChanges(_: SimpleChanges): void { + if (this.initialized) { + this.syncView(); + } } ngAfterViewInit(): void { - this.doRendering(); + this.initialized = true; + this.syncView(); + this.extractLabel(); this.extractValue(); } - ngAfterViewChecked(): void {} - - ngOnChanges(_: SimpleChanges): void { - this.doRendering(); - } - getLabel(): string { if (!this.label) { this.extractLabel(); @@ -130,26 +128,20 @@ export class ReadOnlyFormFieldWrapperComponent implements OnInit, AfterViewInit, this.prefixClickedEmitter.emit(null); } - onContentChange(): void { - this.extractLabel(); - this.extractValue(); - } - - private doRendering(): void { + private syncView(): void { if (!this.originalContent) { return; } if (!this.readonly) { - this.correctWidth(); - return; + this.applyFullWidthStyle(); + } else { + this.changeDetector.detectChanges(); } - - this.changeDetector.detectChanges(); } private extractLabel(): void { if (!this.originalContent || !this.originalContent.nativeElement) { - return null; + return; } const labelElement = this.originalContent.nativeElement.querySelector('mat-label'); this.label = labelElement ? labelElement.innerHTML : 'mat-label is missing!'; @@ -172,11 +164,11 @@ export class ReadOnlyFormFieldWrapperComponent implements OnInit, AfterViewInit, return; } if (form && form.get(formControlName)) { - this.value = form.get(formControlName).getRawValue(); + this.value = form.get(formControlName)?.getRawValue(); } } - private correctWidth(): void { + private applyFullWidthStyle(): void { const formField = this.originalContent.nativeElement.querySelector('mat-form-field'); if (formField) { formField.setAttribute('style', 'width:100%'); diff --git a/projects/material-addons/src/lib/readonly/readonly-form-field/readonly-form-field.component.css b/projects/material-addons/src/lib/readonly/readonly-form-field/readonly-form-field.component.scss similarity index 100% rename from projects/material-addons/src/lib/readonly/readonly-form-field/readonly-form-field.component.css rename to projects/material-addons/src/lib/readonly/readonly-form-field/readonly-form-field.component.scss diff --git a/projects/material-addons/src/lib/readonly/readonly-form-field/readonly-form-field.component.spec.ts b/projects/material-addons/src/lib/readonly/readonly-form-field/readonly-form-field.component.spec.ts new file mode 100644 index 00000000..657b8be2 --- /dev/null +++ b/projects/material-addons/src/lib/readonly/readonly-form-field/readonly-form-field.component.spec.ts @@ -0,0 +1,82 @@ +import { ReadOnlyFormFieldComponent } from './readonly-form-field.component'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NumberFormatService } from '../../numeric-field/number-format.service'; +import { ChangeDetectorRef, ElementRef, Renderer2 } from '@angular/core'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { By } from '@angular/platform-browser'; + +describe('ReadOnlyFormFieldComponent', () => { + let component: ReadOnlyFormFieldComponent; + let fixture: ComponentFixture; + let numberFormatService: NumberFormatService; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ReadOnlyFormFieldComponent, NoopAnimationsModule], + providers: [ + NumberFormatService, + Renderer2, + ChangeDetectorRef, + { provide: ElementRef, useValue: new ElementRef(document.createElement('div')) }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(ReadOnlyFormFieldComponent); + component = fixture.componentInstance; + numberFormatService = TestBed.inject(NumberFormatService); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should fallback to dash if value is not set', () => { + component.value = undefined; + component.ngOnChanges({}); + expect(component.value).toBe('-'); + }); + + it('should format number if formatNumber is true', () => { + const spy = jest.spyOn(numberFormatService, 'format').mockReturnValue('1,234.00'); + component.value = 1234; + component.formatNumber = true; + component.ngOnChanges({}); + expect(spy).toHaveBeenCalled(); + expect(component.value).toBe('1,234.00'); + }); + + it('should emit suffixClickedEmitter when suffix icon is clicked', () => { + const spy = jest.spyOn(component.suffixClickedEmitter, 'emit'); + component.suffix = 'info'; + fixture.detectChanges(); + const suffixIcon = fixture.debugElement.query(By.css('[data-cy="suffix-icon"]')); + suffixIcon.triggerEventHandler('click'); + expect(spy).toHaveBeenCalled(); + }); + + it('should emit prefixClickedEmitter when prefix icon is clicked', () => { + const spy = jest.spyOn(component.prefixClickedEmitter, 'emit'); + component.prefix = 'info'; + fixture.detectChanges(); + const prefixIcon = fixture.debugElement.query(By.css('[data-cy="prefix-icon"]')); + prefixIcon.triggerEventHandler('click'); + expect(spy).toHaveBeenCalled(); + }); + + it('should compute tooltip text correctly for unit position right', () => { + component.value = 123; + component.unit = '%'; + component.unitPosition = 'right'; + const tooltip = component['calculateToolTipText'](); + expect(tooltip).toBe('123 %'); + }); + + it('should compute tooltip text correctly for unit position left', () => { + component.value = 123; + component.unit = '$'; + component.unitPosition = 'left'; + const tooltip = component['calculateToolTipText'](); + expect(tooltip).toBe('$ 123'); + }); +}); diff --git a/projects/material-addons/src/lib/readonly/readonly-form-field/readonly-form-field.component.ts b/projects/material-addons/src/lib/readonly/readonly-form-field/readonly-form-field.component.ts index ace1f5e9..02ce9023 100644 --- a/projects/material-addons/src/lib/readonly/readonly-form-field/readonly-form-field.component.ts +++ b/projects/material-addons/src/lib/readonly/readonly-form-field/readonly-form-field.component.ts @@ -1,5 +1,6 @@ import { - AfterViewChecked, + AfterViewInit, + ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, @@ -30,57 +31,43 @@ import { MatFormFieldModule } from '@angular/material/form-field'; @Component({ selector: 'mad-readonly-form-field', templateUrl: './readonly-form-field.component.html', - styleUrls: ['./readonly-form-field.component.css'], + styleUrls: ['./readonly-form-field.component.scss'], imports: [MatFormFieldModule, NgIf, MatInputModule, FormsModule, NgStyle, NgClass, MatTooltipModule, TextFieldModule, MatIconModule], + changeDetection: ChangeDetectionStrategy.OnPush, }) -export class ReadOnlyFormFieldComponent implements OnChanges, AfterViewChecked { - @ViewChild('contentWrapper', { static: false }) - originalContent: ElementRef; - @Input('useProjectedContent') useProjectedContent: boolean = false; - @Input('value') value?: any; - @Input('label') label: string; - @Input('textAlign') textAlign: 'right' | 'left' = 'left'; - @Input('formatNumber') formatNumber = false; - @Input('decimalPlaces') decimalPlaces = 2; - @Input('roundDisplayValue') roundValue = false; - @Input('autofillDecimals') autofillDecimals = false; - @Input('unit') unit: string | null = null; - @Input('unitPosition') unitPosition: 'right' | 'left' = 'left'; - @Input('errorMessage') errorMessage: string | null = null; +export class ReadOnlyFormFieldComponent implements OnChanges, AfterViewInit { + @Input() useProjectedContent: boolean = false; + @Input() value?: any; + @Input() label!: string; + @Input() textAlign: 'right' | 'left' = 'left'; + @Input() formatNumber = false; + @Input() decimalPlaces = 2; + @Input() autofillDecimals = false; + @Input() unit: string | null = null; + @Input() unitPosition: 'right' | 'left' = 'left'; + @Input() errorMessage: string | null = null; @Input() multiline = false; - @Input() rows: number; - @Input() id: string; - /* - * If shrinkIfEmpty is set to "false", nothing changes - * If set to "true" and multiline is also "true", the textarea will - * shrink to one row, if value is empty/null/undefined. - * Otherwise, the defined rows-value will be used - */ + @Input() rows!: number; @Input() shrinkIfEmpty = false; - /** - * suffix iocon - */ - @Input() suffix: string; - /** - * prefix iocon - */ - @Input() prefix: string; - /** - * if cdkTextareaAutosize is active for textareas - */ + @Input() suffix!: string; + @Input() prefix!: string; @Input() multilineAutoSize = false; + @Input() id!: string; + @Output() suffixClickedEmitter = new EventEmitter(); @Output() prefixClickedEmitter = new EventEmitter(); - @ViewChild('inputEl') inputEl: ElementRef; + @ViewChild('inputEl') inputEl!: ElementRef; + errorMatcher: ErrorStateMatcher = { isErrorState: () => !!this.errorMessage, }; - private unitSpan: HTMLSpanElement; - private textSpan: HTMLSpanElement; + private unitSpan!: HTMLSpanElement; + private textSpan!: HTMLSpanElement; + private viewInitialized = false; toolTipForInputEnabled = false; - toolTipText: string; + toolTipText!: string; constructor( private changeDetector: ChangeDetectorRef, @@ -102,13 +89,16 @@ export class ReadOnlyFormFieldComponent implements OnChanges, AfterViewChecked { autofillDecimals: this.autofillDecimals, }); } - this.changeDetector.detectChanges(); } - // TODO direct copy from NumericFieldDirective - ngAfterViewChecked(): void { + ngAfterViewInit(): void { + if (this.viewInitialized) { + return; + } + + this.viewInitialized = true; this.injectUnitSymbol(); - // If useProjectedContent is set to true, the input wont be show + // If useProjectedContent is set to true, the input not be show if (!this.useProjectedContent) { this.setReadonlyFieldStyle(); this.setTooltipForOverflownField(); @@ -125,7 +115,7 @@ export class ReadOnlyFormFieldComponent implements OnChanges, AfterViewChecked { private injectUnitSymbol(): void { // Need to inject the unit symbol when the input element width is set to its actual value, - // otherwise the icon wont show in the correct position + // otherwise the icon not show in the correct position if (!!this.unit && !this.unitSpan && this.inputEl.nativeElement.offsetWidth !== 0) { // Get the input wrapper and apply necessary styles const inputWrapper = this.inputEl.nativeElement.parentNode.parentNode; @@ -186,13 +176,9 @@ export class ReadOnlyFormFieldComponent implements OnChanges, AfterViewChecked { // Ellipsis is enabled by default as text-overflow behaviour private getTextOverFlowStyleValue(): string { // it works only if the style is added to the component directly. Should find a way for get it from the calculated - // style. Than it would be possible to define the text-overflow in css for the whole application + // style. Then it would be possible to define the text-overflow in css for the whole application const textOverflow = this.elementRef?.nativeElement?.style.textOverflow; - if (!textOverflow) { - return 'ellipsis'; - } - - return textOverflow; + return textOverflow || 'ellipsis'; } private setTooltipForOverflownField(): void { @@ -212,10 +198,7 @@ export class ReadOnlyFormFieldComponent implements OnChanges, AfterViewChecked { } private isTextOverflown(input: any): boolean { - if (input) { - return input.offsetWidth < input.scrollWidth; - } - return false; + return input && input.offsetWidth < input.scrollWidth; } private calculateToolTipText(): string { @@ -223,6 +206,6 @@ export class ReadOnlyFormFieldComponent implements OnChanges, AfterViewChecked { return this.value; } - return this.unitPosition === 'left' ? this.unit + ' ' + this.value : this.value + ' ' + this.unit; + return this.unitPosition === 'left' ? `${this.unit} ${this.value}` : `${this.value} ${this.unit}`; } } diff --git a/projects/material-addons/src/version.ts b/projects/material-addons/src/version.ts index 8ba38100..c1c723c8 100644 --- a/projects/material-addons/src/version.ts +++ b/projects/material-addons/src/version.ts @@ -1 +1 @@ -export const VERSION = '19.0.1'; +export const VERSION = '19.0.3'; diff --git a/src/app/component-demos/alert-demo/alert-demo-api-spec/alert-demo-api-spec.component.scss b/src/app/component-demos/alert-demo/alert-demo-api-spec/alert-demo-api-spec.component.scss deleted file mode 100644 index 421d448b..00000000 --- a/src/app/component-demos/alert-demo/alert-demo-api-spec/alert-demo-api-spec.component.scss +++ /dev/null @@ -1,24 +0,0 @@ -.api-specification { - font-family: Arial, sans-serif; -} - -.api-specification h2 { - color: #333; -} - -.api-specification table { - width: 100%; - border-collapse: collapse; - margin-bottom: 20px; -} - -.api-specification th, -.api-specification td { - border: 1px solid #ccc; - padding: 8px; - text-align: left; -} - -.api-specification th { - background-color: #f2f2f2; -} diff --git a/src/app/component-demos/alert-demo/alert-demo-api-spec/alert-demo-api-spec.component.ts b/src/app/component-demos/alert-demo/alert-demo-api-spec/alert-demo-api-spec.component.ts index f07cf4a8..76621b37 100644 --- a/src/app/component-demos/alert-demo/alert-demo-api-spec/alert-demo-api-spec.component.ts +++ b/src/app/component-demos/alert-demo/alert-demo-api-spec/alert-demo-api-spec.component.ts @@ -5,6 +5,5 @@ import { CommonModule } from '@angular/common'; selector: 'app-alert-demo-api-spec', imports: [CommonModule], templateUrl: './alert-demo-api-spec.component.html', - styleUrl: './alert-demo-api-spec.component.scss', }) export class AlertDemoApiSpecComponent {} diff --git a/src/app/component-demos/read-only-demo/read-only-demo-api-spec/read-only-demo-api-spec.component.html b/src/app/component-demos/read-only-demo/read-only-demo-api-spec/read-only-demo-api-spec.component.html new file mode 100644 index 00000000..3f709e2f --- /dev/null +++ b/src/app/component-demos/read-only-demo/read-only-demo-api-spec/read-only-demo-api-spec.component.html @@ -0,0 +1,137 @@ +
+

Properties

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyTypeDefault ValueDescription
labelstring''Label shown inside the floating mat-label.
valueany'-' if emptyThe value to display. Supports formatting for numbers.
textAlign'left' | 'right''left'Alignment of the text in the field.
formatNumberbooleanfalseIf true, formats value using NumberFormatService.
decimalPlacesnumber2Number of decimal places to display when formatting numbers.
autofillDecimalsbooleanfalseIf true, ensures trailing decimal places are filled.
unitstring | nullnullUnit string (e.g. %, EUR) injected next to the value.
unitPosition'left' | 'right''left'Placement of unit relative to the value.
errorMessagestring | nullnullOptional error message shown as mat-error.
idstring''ID for the input/textarea field.
suffixstring''Material icon name shown as matSuffix.
prefixstring''Material icon name shown as matPrefix.
shrinkIfEmptybooleanfalseShrinks to 1 row in multiline mode if value is empty or undefined.
multilinebooleanfalseIf true, displays a textarea instead of an input.
rowsnumber''Row count for textarea when multiline is true.
multilineAutoSizebooleanfalseEnables Angular CDK auto-sizing on textarea.
useProjectedContentbooleanfalseIf true, hides default rendering and shows projected custom content.
+ +

Events

+ + + + + + + + + + + + + + + + + +
EventDescription
suffixClickedEmitterEmits when suffix icon is clicked.
prefixClickedEmitterEmits when prefix icon is clicked.
+
diff --git a/src/app/component-demos/read-only-demo/read-only-demo-api-spec/read-only-demo-api-spec.component.ts b/src/app/component-demos/read-only-demo/read-only-demo-api-spec/read-only-demo-api-spec.component.ts new file mode 100644 index 00000000..44879a3c --- /dev/null +++ b/src/app/component-demos/read-only-demo/read-only-demo-api-spec/read-only-demo-api-spec.component.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-read-only-demo-api-spec', + imports: [], + templateUrl: './read-only-demo-api-spec.component.html', +}) +export class ReadOnlyDemoApiSpecComponent {} diff --git a/src/app/component-demos/read-only-demo/read-only-demo-api-spec/read-only-wrapper-demo-api-spec.component.html b/src/app/component-demos/read-only-demo/read-only-demo-api-spec/read-only-wrapper-demo-api-spec.component.html new file mode 100644 index 00000000..9d75e8ad --- /dev/null +++ b/src/app/component-demos/read-only-demo/read-only-demo-api-spec/read-only-wrapper-demo-api-spec.component.html @@ -0,0 +1,131 @@ +
+

Properties

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyTypeDefault ValueDescription
readonlybooleantrueIf true, shows the read-only view. If false, shows projected content.
valueany'-' if emptyThe value to display in read-only mode. Auto-extracted if not set.
textAlign'left' | 'right''left'Text alignment for read-only display.
formatNumberbooleanfalseWhether to format numeric value in read-only mode.
decimalPlacesnumber2Decimal precision for formatting numbers.
autofillDecimalsbooleanfalseIf true, fills trailing decimals.
unitstring | nullnullOptional unit symbol to show next to the value (e.g. %, €).
unitPosition'left' | 'right''left'Position of the unit relative to the value.
errorMessagestring | nullnullError text to show under the readonly field.
idstring''DOM ID passed through to the internal field.
suffixstring''Material icon name to use as suffix in read-only view.
prefixstring''Material icon name to use as prefix in read-only view.
shrinkIfEmptybooleanfalseShrinks to 1 row if value is missing and multiline is enabled.
multilinebooleanfalseEnables rendering a textarea in read-only mode.
rowsnumber''Row count for the textarea in multiline mode.
multilineAutoSizebooleanfalseEnables Angular CDK autosizing on multiline readonly field.
+ +

Events

+ + + + + + + + + + + + + + + + + +
EventDescription
suffixClickedEmitterEmits when suffix icon is clicked.
prefixClickedEmitterEmits when prefix icon is clicked.
+
diff --git a/src/app/component-demos/read-only-demo/read-only-demo-api-spec/read-only-wrapper-demo-api-spec.component.ts b/src/app/component-demos/read-only-demo/read-only-demo-api-spec/read-only-wrapper-demo-api-spec.component.ts new file mode 100644 index 00000000..916c6d2c --- /dev/null +++ b/src/app/component-demos/read-only-demo/read-only-demo-api-spec/read-only-wrapper-demo-api-spec.component.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-read-only-wrapper-demo-api-spec', + imports: [], + templateUrl: './read-only-wrapper-demo-api-spec.component.html', +}) +export class ReadOnlyWrapperDemoApiSpecComponent {} diff --git a/src/app/component-demos/read-only-demo/read-only-demo.component.html b/src/app/component-demos/read-only-demo/read-only-demo.component.html index e1b4969e..e82c1d7d 100644 --- a/src/app/component-demos/read-only-demo/read-only-demo.component.html +++ b/src/app/component-demos/read-only-demo/read-only-demo.component.html @@ -1,16 +1,68 @@

- Import the ReadOnlyFormFieldModule in your app.module.ts and use it via - <mad-readonly-form-field> - You can either make a readonly form field unchangeable or changeable by using the readonly flag. + A reusable Angular Material-based read-only field component. Displays a value inside a styled mat-form-field with optional formatting, + tooltips, unit display, and icons.

+

Use When

+
    +
  • Display a formatted, read-only value that aligns with Angular Material form styling.
  • +
  • Show a tooltip on overflow, unit suffixes (like %, €), or prefix/suffix icons.
  • +
  • Reuse consistent read-only field styles across your application.
  • +
+

Behavior

+
    +
  • If value is null, undefined, or empty, it defaults to '-'.
  • +
  • When formatNumber is enabled, the value is formatted using NumberFormatService based on decimalPlaces and autofillDecimals.
  • +
  • + If unit is set, a span is dynamically injected into the mat-form-field and placed before or after the value based on unitPosition. +
  • +
  • The component checks whether the value overflows the field and shows a matTooltip only in that case.
  • +
  • Works seamlessly with Angular Material's layout for form fields, suffixes, prefixes, and errors.
  • +

- Undefined or null values are automatically handled from the readonly form field. Fields where not data is inputted are shown with an "-". + Import the + ReadOnlyFormFieldModule + in your app.module.ts and use it via + <mad-readonly-form-field>

- +

There is als an option to set error messages for the readonly field

- + +

API Specification

+ -

The changeable version needs a wrapper around the form field, therefore, it is a different component.

- +

Readonly Form Field Wrapper

+

A flexible wrapper component that toggles between:

+
    +
  • An editable form field (projected via ng-content).
  • +
  • A read-only view powered by mad-readonly-form-field.
  • +
+

Use When

+
    +
  • Toggle between edit and read-only modes for Angular Material form fields.
  • +
  • Display consistent read-only UI using the same styling, formatting, and layout as your forms.
  • +
+

Behavior

+
    +
  • + If readonly = false: the component renders projected ng-content as-is (e.g. a standard + mat-form-field). +
  • +
  • If readonly = true: it displays mad-readonly-form-field with:
  • +
      +
    • the provided value (or auto-extracted via formControlName)
    • +
    • the extracted mat-label (automatically parsed from projected content)
    • +
    +
  • Maintains full layout consistency with Angular Material components.
  • +
+

+ Import the + ReadOnlyFormFieldModule + in your app.module.ts and use it via + <mad-readonly-form-field-wrapper> +

+

This allows seamless switching between edit and view modes without duplicating layout or logic.

+ +

API Specification

+ diff --git a/src/app/component-demos/read-only-demo/read-only-demo.component.scss b/src/app/component-demos/read-only-demo/read-only-demo.component.scss index 06c78e20..96dbc149 100644 --- a/src/app/component-demos/read-only-demo/read-only-demo.component.scss +++ b/src/app/component-demos/read-only-demo/read-only-demo.component.scss @@ -1,15 +1,5 @@ -@use 'styles'; -@use '@porscheinformatik/material-addons/themes/common/theme'; - -.example { - background: theme.get-selection-background(); - padding: 20px; - margin: 10px; - border-radius: 2px; -} - -.source-code-button { - position: absolute; - right: 10px; - top: 10px; +h1 { + font-size: 40px; + font-weight: 300; + line-height: 1.1em; } diff --git a/src/app/component-demos/read-only-demo/read-only-demo.component.ts b/src/app/component-demos/read-only-demo/read-only-demo.component.ts index e5e78aa4..193c47aa 100644 --- a/src/app/component-demos/read-only-demo/read-only-demo.component.ts +++ b/src/app/component-demos/read-only-demo/read-only-demo.component.ts @@ -5,12 +5,14 @@ import { ReadOnlyFieldWrapperComponent } from '../../example-components/read-onl import { ReadOnlyFieldErrorComponent } from '../../example-components/read-only-field-error/read-only-field-error.component'; import { ExampleViewerComponent } from '../../components/example-viewer/example-viewer.component'; import { TextCodeComponent } from '../../components/text-code/text-code.component'; +import { ReadOnlyDemoApiSpecComponent } from './read-only-demo-api-spec/read-only-demo-api-spec.component'; +import { ReadOnlyWrapperDemoApiSpecComponent } from './read-only-demo-api-spec/read-only-wrapper-demo-api-spec.component'; @Component({ selector: 'app-read-only-demo', templateUrl: './read-only-demo.component.html', styleUrls: ['./read-only-demo.component.scss'], - imports: [TextCodeComponent, ExampleViewerComponent], + imports: [TextCodeComponent, ExampleViewerComponent, ReadOnlyDemoApiSpecComponent, ReadOnlyWrapperDemoApiSpecComponent], }) export class ReadOnlyDemoComponent { readOnlyFormFieldComponent = new Example(ReadOnlyFieldComponent, 'read-only-field', 'Read only form field - unchangeable'); diff --git a/src/app/example-components/read-only-field-error/read-only-field-error.component.html b/src/app/example-components/read-only-field-error/read-only-field-error.component.html index 8f5736ee..6eed8ed3 100644 --- a/src/app/example-components/read-only-field-error/read-only-field-error.component.html +++ b/src/app/example-components/read-only-field-error/read-only-field-error.component.html @@ -1 +1 @@ - + diff --git a/src/app/example-components/read-only-field/read-only-field.component.html b/src/app/example-components/read-only-field/read-only-field.component.html index effebc19..60db2b3b 100644 --- a/src/app/example-components/read-only-field/read-only-field.component.html +++ b/src/app/example-components/read-only-field/read-only-field.component.html @@ -10,7 +10,7 @@ -TADA