src/app/core/common-components/color-input/color-input.component.ts

Description

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.

Extends

CustomFormControlDirective<string>

Implements

EditComponent OnInit

Example

Metadata

Relationships

Index

Properties
Methods
Inputs
Outputs
Accessors

Inputs

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
disabled
Type : boolean
ngControl
Type : any
Default value : inject(NgControl, { optional: true, self: true })
placeholder
Type : string
required
Type : boolean
value
Type : T

Outputs

valueChange
Type : EventEmitter

Methods

onColorPickerChange
onColorPickerChange(value: string)
Parameters :
Name Type Optional
value string No
Returns : void
writeValue
writeValue(value: string, notifyFormControl: unknown)
Parameters :
Name Type Optional Default value
value string No
notifyFormControl unknown No false
Returns : void
blur
blur()
Returns : void
focus
focus()
Returns : void
onContainerClick
onContainerClick(event: MouseEvent)
Parameters :
Name Type Optional
event MouseEvent No
Returns : void
registerOnChange
registerOnChange(fn: any)
Parameters :
Name Type Optional
fn any No
Returns : void
registerOnTouched
registerOnTouched(fn: any)
Parameters :
Name Type Optional
fn any No
Returns : void
setDescribedByIds
setDescribedByIds(ids: string[])
Parameters :
Name Type Optional
ids string[] No
Returns : void
setDisabledState
setDisabledState(isDisabled: boolean)
Parameters :
Name Type Optional
isDisabled boolean No
Returns : void

Properties

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"
elementRef
Type : unknown
Default value : inject<ElementRef<HTMLElement>>(ElementRef)
Readonly enabled
Type : Signal<boolean>
Default value : computed(() => !this._disabled())

Whether the control is currently enabled, as a signal (tracks disabled).

errorStateMatcher
Type : unknown
Default value : inject(ErrorStateMatcher)
id
Type : unknown
Default value : `custom-form-control-${CustomFormControlDirective.nextId++}`
Static nextId
Type : number
Default value : 0
onChange
Type : unknown
Default value : () => {...}
onTouched
Type : unknown
Default value : () => {...}
parentForm
Type : unknown
Default value : inject(NgForm, { optional: true })
parentFormGroup
Type : unknown
Default value : inject(FormGroupDirective, { optional: true })
stateChanges
Type : unknown
Default value : new Subject<void>()
Readonly valueSignal
Type : Signal<T>
Default value : computed(() => this._value())

The current value of the control as a signal. Authoritative in both modes: it reflects the bound FormControl (synced in ngDoCheck) as well as [(value)] / writeValue updates.

Accessors

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>
      &nbsp;
      <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;
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""