src/app/core/default-values/admin-default-value/admin-default-value.component.ts

Description

Admin UI component used in AdminEntityFieldComponent dialog to let users configure different defaultValue modes for an Entity field.

Extends

CustomFormControlDirective<DefaultValueConfig>

Example

Metadata

Relationships

Index

Properties
Methods
Inputs
Outputs

Constructor

constructor()

Inputs

entitySchemaField
Type : EntitySchemaField
Required :  true
entityType
Type : EntityConstructor
Required :  true
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

clearDefaultValue
clearDefaultValue()
Returns : void
writeValue
writeValue(value: DefaultValueConfig, notifyFormControl: unknown)
Parameters :
Name Type Optional Default value
value DefaultValueConfig 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

form
Type : unknown
Default value : new FormGroup({ mode: this.modeControl, config: this.configControl, })
modes
Type : unknown
Default value : computed(() => this.modesResource.value() ?? [])
selectedMode
Type : unknown
Default value : signal<DefaultValueMode | null>(null)
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.

import {
  computed,
  Component,
  inject,
  ChangeDetectionStrategy,
  input,
  resource,
  signal,
} from "@angular/core";
import { DefaultValueConfig, DefaultValueMode } from "../default-value-config";
import {
  MatError,
  MatFormField,
  MatFormFieldControl,
  MatLabel,
  MatSuffix,
} from "@angular/material/form-field";
import {
  FormControl,
  FormGroup,
  FormsModule,
  ReactiveFormsModule,
  ValidatorFn,
} from "@angular/forms";
import {
  MatButtonToggle,
  MatButtonToggleGroup,
} from "@angular/material/button-toggle";
import {
  AdminDefaultValueContext,
  DefaultValueStrategy,
} from "../default-value-strategy.interface";
import { MatTooltip } from "@angular/material/tooltip";
import { FaIconComponent } from "@fortawesome/angular-fontawesome";
import { FaDynamicIconComponent } from "../../common-components/fa-dynamic-icon/fa-dynamic-icon.component";
import { MatIconButton } from "@angular/material/button";
import { EntityConstructor } from "../../entity/model/entity";
import { CustomFormControlDirective } from "../../common-components/basic-autocomplete/custom-form-control.directive";
import { AdminDefaultValueDynamicComponent } from "../x-dynamic-placeholder/admin-default-value-dynamic/admin-default-value-dynamic.component";
import { AdminDefaultValueStaticComponent } from "../x-static/admin-default-value-static/admin-default-value-static.component";
import { EntitySchemaField } from "../../entity/schema/entity-schema-field";
import { AdminInheritedFieldComponent } from "../../../features/inherited-field/admin-inherited-field/admin-inherited-field.component";

/**
 * Admin UI component used in AdminEntityFieldComponent dialog
 * to let users configure different defaultValue modes for an Entity field.
 */
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: "app-admin-default-value",
  imports: [
    MatFormField,
    MatLabel,
    MatError,
    ReactiveFormsModule,
    FormsModule,
    MatButtonToggleGroup,
    MatButtonToggle,
    MatTooltip,
    FaDynamicIconComponent,
    FaIconComponent,
    MatSuffix,
    MatIconButton,
    AdminDefaultValueDynamicComponent,
    AdminDefaultValueStaticComponent,
    AdminInheritedFieldComponent,
  ],
  templateUrl: "./admin-default-value.component.html",
  styleUrl: "./admin-default-value.component.scss",
  providers: [
    { provide: MatFormFieldControl, useExisting: AdminDefaultValueComponent },
  ],
})
export class AdminDefaultValueComponent extends CustomFormControlDirective<DefaultValueConfig> {
  entityType = input.required<EntityConstructor>();
  entitySchemaField = input.required<EntitySchemaField>();

  private readonly modeControl = new FormControl<DefaultValueMode | null>(null);
  private readonly configControl = new FormControl<
    DefaultValueConfig["config"] | null
  >(null, {
    validators: [this.requiredIfModeSelected()],
  });

  form = new FormGroup({
    mode: this.modeControl,
    config: this.configControl,
  });

  private defaultValueStrategies = inject(
    DefaultValueStrategy,
  ) as unknown as DefaultValueStrategy[];

  private readonly modesResource = resource<
    AdminDefaultValueContext[],
    unknown
  >({
    loader: async () =>
      Promise.all(
        this.defaultValueStrategies.map((strategy) => strategy.getAdminUI()),
      ),
  });

  modes = computed(() => this.modesResource.value() ?? []);

  selectedMode = signal<DefaultValueMode | null>(null);

  constructor() {
    super();
    this.syncFormWithValue(this.value);

    this.form.get("mode")?.valueChanges.subscribe((mode) => {
      this.selectedMode.set(mode);
      this.form.get("config")?.setValue(null);
    });

    this.form.get("config")?.valueChanges.subscribe((value) => {
      if (!this.form.get("mode")?.getRawValue() && !!value) {
        // set default mode as "static" after user started typing a value
        this.form.get("mode")?.setValue("static", { emitEvent: false });
      }
    });

    this.form.valueChanges.subscribe(() => this.updateValue());
  }

  override writeValue(value: DefaultValueConfig, notifyFormControl = false) {
    super.writeValue(value, notifyFormControl);
    this.syncFormWithValue(value);
  }

  private syncFormWithValue(value: DefaultValueConfig | null | undefined) {
    const newFormValue = {
      mode: value?.mode ?? null,
      config: value?.config ?? null,
    };

    if (
      JSON.stringify(this.form.getRawValue()) !== JSON.stringify(newFormValue)
    ) {
      this.form.setValue(newFormValue, { emitEvent: false });
      this.form.updateValueAndValidity({ emitEvent: false });
      this.selectedMode.set(newFormValue.mode);
    }
  }

  private updateValue() {
    this.form.markAllAsTouched();
    if (this.form.invalid) {
      // TODO: make sure the admin components of each mode are correctly setting themselves as invalid
      return;
    }

    let newConfigValue: DefaultValueConfig = this.form.getRawValue();

    // output as `null` if no value is set to conform with standard form controls
    if (!newConfigValue || (!newConfigValue.mode && !newConfigValue.config)) {
      newConfigValue = null;
    }

    if (JSON.stringify(newConfigValue) !== JSON.stringify(this.value)) {
      this.value = newConfigValue;
    }
  }

  private requiredIfModeSelected(): ValidatorFn {
    return (control) => {
      if (this.modeControl.value && !control.value) {
        return { requiredForMode: true };
      }
      return null;
    };
  }

  clearDefaultValue() {
    // TODO: this causes the config to keep a defaultValue with `config: { value: null }` instead of returning just `null` and deleting the property
    // Need to fix so that the parent form control gets reset to null (without causing errors in the component here internally)
    this.form.setValue({ mode: null, config: null });
  }
}
<mat-form-field [formGroup]="form" floatLabel="always">
  <mat-label i18n>Default Value</mat-label>

  <div class="flex-row gap-regular align-center">
    <div class="flex-grow">
      @switch (selectedMode()) {
        @default {
          <app-admin-default-value-static
            formControlName="config"
            [entitySchemaField]="entitySchemaField()"
          ></app-admin-default-value-static>
        }
        @case ("dynamic") {
          <app-admin-default-value-dynamic
            formControlName="config"
          ></app-admin-default-value-dynamic>
        }
        @case ("inherited-field") {
          <app-admin-inherited-field
            formControlName="config"
            [entityType]="entityType()"
            [entitySchemaField]="entitySchemaField()"
          ></app-admin-inherited-field>
        }
      }
    </div>

    <mat-button-toggle-group
      formControlName="mode"
      i18n-aria-label
      aria-label="default value mode"
      hideSingleSelectionIndicator
      style="flex: none"
    >
      @for (strategy of modes(); track strategy.mode) {
        <mat-button-toggle
          [value]="strategy.mode"
          [matTooltip]="strategy.description"
        >
          <app-fa-dynamic-icon [icon]="strategy.icon"></app-fa-dynamic-icon>
        </mat-button-toggle>
      }
    </mat-button-toggle-group>

    <button
      mat-icon-button
      matIconSuffix
      matTooltip="Remove default value"
      i18n-matTooltip
      (click)="clearDefaultValue()"
      color="{{ form.invalid ? 'accent' : '' }}"
    >
      <fa-icon icon="times"></fa-icon>
    </button>
  </div>

  @if (form.invalid) {
    <mat-error i18n="error message"
      >Select and fully configure default value or clear the mode
    </mat-error>
  }
</mat-form-field>

./admin-default-value.component.scss

:host {
  min-width: 450px;
}

mat-form-field {
  width: 100%;
}

mat-option {
  font-style: italic;
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""