src/app/core/common-components/color-input/color-input.component.ts
Edit component for color fields. Can be used as an EditComponent in entity forms (registered as "EditColor"), or as a standalone component with [value]/(valueChange) bindings.
CustomFormControlDirective<string>
| changeDetection | ChangeDetectionStrategy.OnPush |
| providers |
ColorInputComponent
|
| selector | app-color-input |
| standalone | true |
| imports |
MatFormFieldModule
MatInputModule
MatButtonModule
FontAwesomeModule
MatTooltipModule
|
| styleUrls | ./color-input.component.scss |
| templateUrl | ./color-input.component.html |
| styleUrl | ./color-input.component.scss |
ReactiveFormsModule
MatFormFieldModule
MatInputModule
MatButtonModule
FontAwesomeModule
MatTooltipModule
NgTemplateOutlet
CustomFormControlDirective<string>
EditComponent
OnInit
Properties |
|
Methods |
Inputs |
Outputs |
Accessors |
| compact | |
Type : boolean
|
|
Default value : false
|
|
|
If true, renders only the compact color picker button (no label, no text field, no form field). Useful for inline/icon-only usage. |
|
| formFieldConfig | |
Type : FormFieldConfig
|
|
| label | |
Type : string
|
|
Default value : $localize`Color`
|
|
|
Label for the color input field (used in standalone full mode). |
|
| aria-describedby | |
Type : string
|
|
|
Inherited from
CustomFormControlDirective
|
|
| disabled | |
Type : boolean
|
|
|
Inherited from
CustomFormControlDirective
|
|
| ngControl | |
Type : any
|
|
Default value : inject(NgControl, { optional: true, self: true })
|
|
|
Inherited from
CustomFormControlDirective
|
|
| placeholder | |
Type : string
|
|
|
Inherited from
CustomFormControlDirective
|
|
| required | |
Type : boolean
|
|
|
Inherited from
CustomFormControlDirective
|
|
| value | |
Type : T
|
|
|
Inherited from
CustomFormControlDirective
|
|
| valueChange | |
Type : EventEmitter
|
|
|
Inherited from
CustomFormControlDirective
|
|
| onColorPickerChange | ||||||
onColorPickerChange(value: string)
|
||||||
|
Parameters :
Returns :
void
|
| writeValue | ||||||||||||
writeValue(value: string, notifyFormControl: unknown)
|
||||||||||||
|
Inherited from
CustomFormControlDirective
|
||||||||||||
|
Parameters :
Returns :
void
|
| blur |
blur()
|
|
Inherited from
CustomFormControlDirective
|
|
Returns :
void
|
| focus |
focus()
|
|
Inherited from
CustomFormControlDirective
|
|
Returns :
void
|
| onContainerClick | ||||||
onContainerClick(event: MouseEvent)
|
||||||
|
Inherited from
CustomFormControlDirective
|
||||||
|
Parameters :
Returns :
void
|
| registerOnChange | ||||||
registerOnChange(fn: any)
|
||||||
|
Inherited from
CustomFormControlDirective
|
||||||
|
Parameters :
Returns :
void
|
| registerOnTouched | ||||||
registerOnTouched(fn: any)
|
||||||
|
Inherited from
CustomFormControlDirective
|
||||||
|
Parameters :
Returns :
void
|
| setDescribedByIds | ||||||
setDescribedByIds(ids: string[])
|
||||||
|
Inherited from
CustomFormControlDirective
|
||||||
|
Parameters :
Returns :
void
|
| setDisabledState | ||||||
setDisabledState(isDisabled: boolean)
|
||||||
|
Inherited from
CustomFormControlDirective
|
||||||
|
Parameters :
Returns :
void
|
| Readonly colorControl |
Type : unknown
|
Default value : new FormControl<string>("", { nonNullable: true })
|
|
Internal form control for the text input in standalone mode. |
| HEX_COLOR_PATTERN |
Type : unknown
|
Default value : /^#[0-9A-Fa-f]{6}$/
|
| controlType |
Type : string
|
Default value : "custom-control"
|
|
Inherited from
CustomFormControlDirective
|
| elementRef |
Type : unknown
|
Default value : inject<ElementRef<HTMLElement>>(ElementRef)
|
|
Inherited from
CustomFormControlDirective
|
| Readonly enabled |
Type : Signal<boolean>
|
Default value : computed(() => !this._disabled())
|
|
Inherited from
CustomFormControlDirective
|
|
Whether the control is currently enabled, as a signal (tracks disabled). |
| errorStateMatcher |
Type : unknown
|
Default value : inject(ErrorStateMatcher)
|
|
Inherited from
CustomFormControlDirective
|
| id |
Type : unknown
|
Default value : `custom-form-control-${CustomFormControlDirective.nextId++}`
|
|
Inherited from
CustomFormControlDirective
|
| Static nextId |
Type : number
|
Default value : 0
|
|
Inherited from
CustomFormControlDirective
|
| onChange |
Type : unknown
|
Default value : () => {...}
|
|
Inherited from
CustomFormControlDirective
|
| onTouched |
Type : unknown
|
Default value : () => {...}
|
|
Inherited from
CustomFormControlDirective
|
| parentForm |
Type : unknown
|
Default value : inject(NgForm, { optional: true })
|
|
Inherited from
CustomFormControlDirective
|
| parentFormGroup |
Type : unknown
|
Default value : inject(FormGroupDirective, { optional: true })
|
|
Inherited from
CustomFormControlDirective
|
| stateChanges |
Type : unknown
|
Default value : new Subject<void>()
|
|
Inherited from
CustomFormControlDirective
|
| Readonly valueSignal |
Type : Signal<T>
|
Default value : computed(() => this._value())
|
|
Inherited from
CustomFormControlDirective
|
|
The current value of the control as a signal.
Authoritative in both modes: it reflects the bound |
| colorPickerValue |
getcolorPickerValue()
|
import {
ChangeDetectionStrategy,
Component,
DestroyRef,
inject,
input,
OnInit,
} from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { filter } from "rxjs";
import { FormControl, ReactiveFormsModule } from "@angular/forms";
import {
MatFormFieldControl,
MatFormFieldModule,
} from "@angular/material/form-field";
import { MatInputModule } from "@angular/material/input";
import { MatButtonModule } from "@angular/material/button";
import { FontAwesomeModule } from "@fortawesome/angular-fontawesome";
import { MatTooltipModule } from "@angular/material/tooltip";
import { NgTemplateOutlet } from "@angular/common";
import { CustomFormControlDirective } from "#src/app/core/common-components/basic-autocomplete/custom-form-control.directive";
import { DynamicComponent } from "#src/app/core/config/dynamic-components/dynamic-component.decorator";
import { EditComponent } from "#src/app/core/entity/entity-field-edit/dynamic-edit/edit-component.interface";
import { FormFieldConfig } from "#src/app/core/common-components/entity-form/FormConfig";
/**
* Edit component for color fields.
* Can be used as an EditComponent in entity forms (registered as "EditColor"),
* or as a standalone component with [value]/(valueChange) bindings.
*/
@DynamicComponent("EditColor")
@Component({
selector: "app-color-input",
standalone: true,
templateUrl: "./color-input.component.html",
styleUrl: "./color-input.component.scss",
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
ReactiveFormsModule,
MatFormFieldModule,
MatInputModule,
MatButtonModule,
FontAwesomeModule,
MatTooltipModule,
NgTemplateOutlet,
],
providers: [
{ provide: MatFormFieldControl, useExisting: ColorInputComponent },
],
})
export class ColorInputComponent
extends CustomFormControlDirective<string>
implements EditComponent, OnInit
{
formFieldConfig = input<FormFieldConfig>();
/**
* If true, renders only the compact color picker button (no label, no text field, no form field).
* Useful for inline/icon-only usage.
*/
compact = input<boolean>(false);
/**
* Label for the color input field (used in standalone full mode).
*/
label = input<string>($localize`Color`);
private readonly destroyRef = inject(DestroyRef);
HEX_COLOR_PATTERN = /^#[0-9A-Fa-f]{6}$/;
/**
* Internal form control for the text input in standalone mode.
*/
readonly colorControl = new FormControl<string>("", { nonNullable: true });
ngOnInit() {
if (this.formControl) {
this.formControl.valueChanges
.pipe(
takeUntilDestroyed(this.destroyRef),
filter(() => !this.formControl.disabled),
)
.subscribe((value) => this.validateHex(value));
} else {
this.colorControl.valueChanges
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((value) => {
if (!value || this.HEX_COLOR_PATTERN.test(value)) {
this.colorControl.setErrors(null);
super.writeValue(value, false);
} else {
this.colorControl.setErrors({ pattern: true });
}
});
}
}
override writeValue(value: string, notifyFormControl = false): void {
if (!this.formControl) {
if (this.colorControl.value !== (value ?? "")) {
this.colorControl.setValue(value ?? "", { emitEvent: false });
}
}
super.writeValue(value, notifyFormControl);
}
onColorPickerChange(value: string) {
if (this.formControl) {
this.formControl.setValue(value);
} else {
this.colorControl.setValue(value);
}
}
private validateHex(value: string): void {
if (!value || this.HEX_COLOR_PATTERN.test(value)) {
this.formControl?.setErrors(null);
} else {
this.formControl?.setErrors({
invalidHex: {
errorMessage: $localize`Please enter a valid hex color code (e.g. #ff0000)`,
},
});
}
}
get colorPickerValue(): string {
const val = this.formControl?.value ?? this.colorControl.value;
return val && this.HEX_COLOR_PATTERN.test(val) ? val : "#000000";
}
}
<!-- Color Picker Button -->
<ng-template #colorPickerButton>
<button
mat-icon-button
type="button"
[style.color]="colorPickerValue"
(click)="colorInput.click()"
tabindex="-1"
matTooltip="Pick color"
i18n-matTooltip
>
<fa-icon icon="palette"></fa-icon>
</button>
<input
#colorInput
type="color"
class="color-input-overlay"
[value]="colorPickerValue"
(input)="onColorPickerChange($any($event.target).value)"
(click)="$event.stopPropagation()"
/>
</ng-template>
@if (ngControl) {
<!-- EditComponent mode: entity form provides the outer mat-form-field -->
<!-- eslint-disable @angular-eslint/template/i18n -- "#RRGGBB" is a colour format, not prose -->
<input
matInput
[formControl]="formControl"
placeholder="#RRGGBB"
class="color-text-input"
/>
<!-- eslint-enable @angular-eslint/template/i18n -->
<ng-container *ngTemplateOutlet="colorPickerButton"></ng-container>
} @else if (compact()) {
<!-- Compact standalone mode: just the color picker button -->
<ng-container *ngTemplateOutlet="colorPickerButton"></ng-container>
} @else {
<!-- Full standalone mode: own mat-form-field with label and error -->
<mat-form-field class="full-width" floatLabel="always">
<mat-label>
<span>{{ label() }}</span>
<fa-icon
icon="question-circle"
matTooltip="Pick a color or enter a color code (e.g. #ff0000)."
i18n-matTooltip
></fa-icon>
</mat-label>
<!-- eslint-disable @angular-eslint/template/i18n -- "#RRGGBB" is a colour format, not prose -->
<input
matInput
[formControl]="colorControl"
style="width: 100px"
placeholder="#RRGGBB"
/>
<!-- eslint-enable @angular-eslint/template/i18n -->
<span matSuffix>
<ng-container *ngTemplateOutlet="colorPickerButton"></ng-container>
</span>
@if (colorControl.hasError("pattern")) {
<mat-error i18n
>Please enter a valid hex color code (e.g. #ff0000)</mat-error
>
}
</mat-form-field>
}
./color-input.component.scss
:host {
display: flex;
align-items: center;
gap: 4px;
width: 100%;
}
.color-text-input {
flex: 1;
}
.color-input-overlay {
position: absolute;
top: 0;
left: 0;
width: 36px;
height: 36px;
opacity: 0;
cursor: pointer;
border: none;
padding: 0;
margin: 0;
z-index: 2;
}