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: `
+
+ `,
+ 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
+
+
+
+ | Property |
+ Type |
+ Default Value |
+ Description |
+
+
+
+
+ label |
+ string |
+ '' |
+ Label shown inside the floating mat-label. |
+
+
+ value |
+ any |
+ '-' if empty |
+ The value to display. Supports formatting for numbers. |
+
+
+ textAlign |
+ 'left' | 'right' |
+ 'left' |
+ Alignment of the text in the field. |
+
+
+ formatNumber |
+ boolean |
+ false |
+ If true, formats value using NumberFormatService. |
+
+
+ decimalPlaces |
+ number |
+ 2 |
+ Number of decimal places to display when formatting numbers. |
+
+
+ autofillDecimals |
+ boolean |
+ false |
+ If true, ensures trailing decimal places are filled. |
+
+
+ unit |
+ string | null |
+ null |
+ Unit string (e.g. %, EUR) injected next to the value. |
+
+
+ unitPosition |
+ 'left' | 'right' |
+ 'left' |
+ Placement of unit relative to the value. |
+
+
+ errorMessage |
+ string | null |
+ null |
+ Optional error message shown as mat-error. |
+
+
+ id |
+ string |
+ '' |
+ ID for the input/textarea field. |
+
+
+ suffix |
+ string |
+ '' |
+ Material icon name shown as matSuffix. |
+
+
+ prefix |
+ string |
+ '' |
+ Material icon name shown as matPrefix. |
+
+
+ shrinkIfEmpty |
+ boolean |
+ false |
+ Shrinks to 1 row in multiline mode if value is empty or undefined. |
+
+
+ multiline |
+ boolean |
+ false |
+ If true, displays a textarea instead of an input. |
+
+
+ rows |
+ number |
+ '' |
+ Row count for textarea when multiline is true. |
+
+
+ multilineAutoSize |
+ boolean |
+ false |
+ Enables Angular CDK auto-sizing on textarea. |
+
+
+ useProjectedContent |
+ boolean |
+ false |
+ If true, hides default rendering and shows projected custom content. |
+
+
+
+
+
Events
+
+
+
+ | Event |
+ Description |
+
+
+
+
+ suffixClickedEmitter |
+ Emits when suffix icon is clicked. |
+
+
+ prefixClickedEmitter |
+ Emits 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
+
+
+
+ | Property |
+ Type |
+ Default Value |
+ Description |
+
+
+
+
+ readonly |
+ boolean |
+ true |
+ If true, shows the read-only view. If false, shows projected content. |
+
+
+ value |
+ any |
+ '-' if empty |
+ The value to display in read-only mode. Auto-extracted if not set. |
+
+
+ textAlign |
+ 'left' | 'right' |
+ 'left' |
+ Text alignment for read-only display. |
+
+
+ formatNumber |
+ boolean |
+ false |
+ Whether to format numeric value in read-only mode. |
+
+
+ decimalPlaces |
+ number |
+ 2 |
+ Decimal precision for formatting numbers. |
+
+
+ autofillDecimals |
+ boolean |
+ false |
+ If true, fills trailing decimals. |
+
+
+ unit |
+ string | null |
+ null |
+ Optional unit symbol to show next to the value (e.g. %, €). |
+
+
+ unitPosition |
+ 'left' | 'right' |
+ 'left' |
+ Position of the unit relative to the value. |
+
+
+ errorMessage |
+ string | null |
+ null |
+ Error text to show under the readonly field. |
+
+
+ id |
+ string |
+ '' |
+ DOM ID passed through to the internal field. |
+
+
+ suffix |
+ string |
+ '' |
+ Material icon name to use as suffix in read-only view. |
+
+
+ prefix |
+ string |
+ '' |
+ Material icon name to use as prefix in read-only view. |
+
+
+ shrinkIfEmpty |
+ boolean |
+ false |
+ Shrinks to 1 row if value is missing and multiline is enabled. |
+
+
+ multiline |
+ boolean |
+ false |
+ Enables rendering a textarea in read-only mode. |
+
+
+ rows |
+ number |
+ '' |
+ Row count for the textarea in multiline mode. |
+
+
+ multilineAutoSize |
+ boolean |
+ false |
+ Enables Angular CDK autosizing on multiline readonly field. |
+
+
+
+
+
Events
+
+
+
+ | Event |
+ Description |
+
+
+
+
+ suffixClickedEmitter |
+ Emits when suffix icon is clicked. |
+
+
+ prefixClickedEmitter |
+ Emits 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