File

src/app/core/default-values/inherited-value.service.ts

Description

An advanced default-value strategy that sets values based on the value in a referenced related entity.

This allows to configure hierarchies and inherited fields, e.g. setting the category field based on the category field in a linked "parent entity".

Extends

DefaultValueStrategy

Index

Methods

Constructor

constructor(entityMapper: EntityMapperService)
Parameters :
Name Type Optional
entityMapper EntityMapperService No

Methods

getDefaultValueUiHint
getDefaultValueUiHint(form: EntityForm<T>, field: FormFieldConfig)
Type parameters :
  • T

Get details about the status and context of an inherited value field to display to the user.

Parameters :
Name Type Optional
form EntityForm<T> No
field FormFieldConfig No
Async initEntityForm
initEntityForm(form: EntityForm<T>)
Inherited from DefaultValueStrategy
Type parameters :
  • T
Parameters :
Name Type Optional
form EntityForm<T> No
Returns : any
Async onFormValueChanges
onFormValueChanges(form: EntityForm<T>)
Inherited from DefaultValueStrategy
Type parameters :
  • T
Parameters :
Name Type Optional
form EntityForm<T> No
Returns : any
Async setDefaultValue
setDefaultValue(targetFormControl: AbstractControl, fieldConfig: EntitySchemaField, form: EntityForm<any>)
Inherited from DefaultValueStrategy

Set up the inheritance value for a field, triggering an initial inheritance and watching future changes of the source (parent) field for automatic updates.

Parameters :
Name Type Optional
targetFormControl AbstractControl<any | any> No
fieldConfig EntitySchemaField No
form EntityForm<any> No
Returns : any
import { Injectable } from "@angular/core";
import { AbstractControl } from "@angular/forms";
import { EntitySchemaField } from "../entity/schema/entity-schema-field";
import { EntityForm } from "../common-components/entity-form/entity-form.service";
import { Entity } from "../entity/model/entity";
import {
  DefaultValueStrategy,
  getConfigsByMode,
} from "./default-value-strategy.interface";
import { EntityMapperService } from "../entity/entity-mapper/entity-mapper.service";
import { DefaultValueConfig } from "../entity/schema/default-value-config";
import { DefaultValueHint } from "./default-value.service";
import { asArray } from "app/utils/asArray";
import { FormFieldConfig } from "../common-components/entity-form/FormConfig";

/**
 * An advanced default-value strategy that sets values based on the value in a referenced related entity.
 *
 * This allows to configure hierarchies and inherited fields,
 * e.g. setting the category field based on the category field in a linked "parent entity".
 */
@Injectable({
  providedIn: "root",
})
export class InheritedValueService extends DefaultValueStrategy {
  constructor(private entityMapper: EntityMapperService) {
    super();
  }

  override async initEntityForm<T extends Entity>(form: EntityForm<T>) {
    await this.updateLinkedEntities(form);
  }

  /**
   * Set up the inheritance value for a field, triggering an initial inheritance
   * and watching future changes of the source (parent) field for automatic updates.
   * @param targetFormControl
   * @param fieldConfig
   * @param form
   */
  async setDefaultValue(
    targetFormControl: AbstractControl<any, any>,
    fieldConfig: EntitySchemaField,
    form: EntityForm<any>,
  ) {
    // load inherited from initial entity
    await this.onSourceValueChange(
      form,
      targetFormControl,
      fieldConfig,
      this.getParentRefId(form, fieldConfig.defaultValue),
    );

    // subscribe to update inherited whenever source field changes
    let sourceFormControl: AbstractControl<any, any> | null =
      form.formGroup.get(fieldConfig.defaultValue.localAttribute);
    if (sourceFormControl && targetFormControl) {
      form.watcher.set(
        "sourceFormControlValueChanges_" +
          fieldConfig.defaultValue.localAttribute,
        sourceFormControl.valueChanges.subscribe(
          async (change) =>
            await this.onSourceValueChange(
              form,
              targetFormControl,
              fieldConfig,
              change,
            ),
        ),
      );
    }
  }

  /**
   * Update the inherited (target field) value based on the change of the source field (parent reference) change.
   * @param form
   * @param targetFormControl
   * @param fieldConfig
   * @param change The new entity ID value of the source (parent ref) field.
   * @private
   */
  private async onSourceValueChange(
    form: EntityForm<any>,
    targetFormControl: AbstractControl<any, any>,
    fieldConfig: EntitySchemaField,
    change,
  ) {
    if (form.formGroup.disabled) {
      return;
    }

    if (
      targetFormControl.dirty &&
      !!targetFormControl.value &&
      form.entity.isNew
    ) {
      return;
    }

    if (!form.entity.isNew) {
      return;
    }

    if (!change || "") {
      targetFormControl.setValue(undefined);
      return;
    }

    // source field is array, use first element if only one element
    if (Array.isArray(change)) {
      if (change.length === 1) {
        change = change[0];
      } else {
        targetFormControl.setValue(undefined);
        return;
      }
    }

    let parentEntity: Entity = await this.entityMapper.load(
      Entity.extractTypeFromId(change),
      change,
    );

    if (
      !parentEntity ||
      parentEntity[fieldConfig.defaultValue.field] === undefined
    ) {
      return;
    }

    if (fieldConfig.isArray) {
      targetFormControl.setValue([
        ...parentEntity[fieldConfig.defaultValue.field],
      ]);
    } else {
      targetFormControl.setValue(parentEntity[fieldConfig.defaultValue.field]);
    }

    targetFormControl.markAsUntouched();
    targetFormControl.markAsPristine();
  }

  override async onFormValueChanges<T extends Entity>(form: EntityForm<T>) {
    await this.updateLinkedEntities(form);
  }

  /**
   * Get details about the status and context of an inherited value field
   * to display to the user.
   * @param form
   * @param field
   */
  getDefaultValueUiHint<T extends Entity>(
    form: EntityForm<T>,
    field: FormFieldConfig,
  ): DefaultValueHint | undefined {
    const defaultConfig = field?.defaultValue;
    if (!defaultConfig) {
      return;
    }

    const parentRefValue = this.getParentRefId(form, defaultConfig);
    if (!parentRefValue) {
      return {
        inheritedFromField: defaultConfig.localAttribute,
        isEmpty: true,
      };
    }

    return {
      inheritedFromField: defaultConfig.localAttribute,
      inheritedFromType: parentRefValue
        ? Entity.extractTypeFromId(parentRefValue)
        : undefined,
      isInSync:
        JSON.stringify(form.inheritedParentValues.get(field.id)) ===
        JSON.stringify(form.formGroup.get(field.id)?.value),
      syncFromParentField: () => {
        form.formGroup
          .get(field.id)
          .setValue(form.inheritedParentValues.get(field.id));
      },
    };
  }

  private async updateLinkedEntities<T extends Entity>(form: EntityForm<T>) {
    let inheritedConfigs: Map<string, DefaultValueConfig> = getConfigsByMode(
      form.fieldConfigs,
      ["inherited"],
    );

    const linkedEntityRefs: Map<string, string[]> = this.getLinkedEntityRefs(
      inheritedConfigs,
      form,
    );

    for (let [fieldId, parentEntityIds] of linkedEntityRefs) {
      if (parentEntityIds.length > 1) {
        // multi-inheritance not supported (yet) -> keep values in form and stop inheritance
        form.inheritedParentValues.delete(fieldId);
        continue;
      }

      let parentEntity: Entity;
      if (parentEntityIds.length === 1) {
        parentEntity = await this.entityMapper.load(
          Entity.extractTypeFromId(parentEntityIds[0]),
          parentEntityIds[0],
        );
      }

      // if value empty -> set inherited values to undefined
      form.inheritedParentValues.set(
        fieldId,
        parentEntity?.[inheritedConfigs.get(fieldId).field],
      );
    }
  }

  /**
   * Get the linked entity references from the form.
   * @param inheritedConfigs
   * @param form
   * @return Entity ids of the linked parent entities (always wrapped as an array)
   * @private
   */
  private getLinkedEntityRefs<T extends Entity>(
    inheritedConfigs: Map<string, DefaultValueConfig>,
    form: EntityForm<T>,
  ): Map<string, string[]> {
    const linkedEntityRefs: Map<string, string[]> = new Map();

    for (const [key, defaultValueConfig] of inheritedConfigs) {
      let linkedEntities: null | string | string[] = this.getParentRefId(
        form,
        defaultValueConfig,
        false,
      );

      if (linkedEntities == null || linkedEntities.length == 0) {
        continue;
      }

      linkedEntityRefs.set(key, asArray(linkedEntities));
    }

    return linkedEntityRefs;
  }

  /**
   * Get the parent reference id from the form or entity.
   * @param form
   * @param defaultConfig
   * @param castToSingle Whether for arrays of IDs, this should be cast to a single ID only.
   * @private
   */
  private getParentRefId<T extends Entity>(
    form: EntityForm<T>,
    defaultConfig: DefaultValueConfig,
    castToSingle = true,
  ): string | undefined {
    const linkedFieldValue =
      form.formGroup?.get(defaultConfig.localAttribute)?.value ??
      form.entity?.[defaultConfig.localAttribute];

    if (!castToSingle) {
      return linkedFieldValue;
    }

    if (Array.isArray(linkedFieldValue)) {
      // inheritance is only supported for a single parent reference
      return linkedFieldValue?.length === 1 ? linkedFieldValue[0] : undefined;
    } else {
      return linkedFieldValue;
    }
  }
}

results matching ""

    No results matching ""