chromium/chrome/browser/resources/settings/autofill_page/address_edit_dialog_components.ts

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

function isValueEmpty(value: string|undefined): boolean {
  return value === undefined || value === '';
}

/**
 * The base class for data behind an address component. It exposes the `value`
 * property, which is how interface controls (e.g. input) communicate with it.
 */
export class AddressComponentUi {
  private readonly fieldType_: chrome.autofillPrivate.FieldType;
  private readonly originalValue_?: string;
  private readonly existingAddress_: boolean;
  private readonly onValueUpdateListener_: () => void;
  private readonly skipValidation_: boolean;
  private addressFields_:
      Map<chrome.autofillPrivate.FieldType, string|undefined>;
  private isValidatable_: boolean;
  readonly isTextarea: boolean;
  readonly isRequired: boolean;
  readonly label: string;
  readonly additionalClassName: string;

  constructor(
      addressFields: Map<chrome.autofillPrivate.FieldType, string|undefined>,
      originalFields: Map<chrome.autofillPrivate.FieldType, string|undefined>|
      undefined,
      fieldType: chrome.autofillPrivate.FieldType,
      label: string,
      onValueUpdateListener: () => void,
      additionalClassName: string = '',
      isTextarea: boolean = false,
      skipValidation: boolean = false,
      isRequired: boolean = false,
  ) {
    this.addressFields_ = addressFields;
    this.existingAddress_ = originalFields !== undefined;
    this.originalValue_ = originalFields?.get(fieldType);
    this.fieldType_ = fieldType;
    this.label = label;
    this.onValueUpdateListener_ = onValueUpdateListener;
    this.additionalClassName = additionalClassName;
    this.isTextarea = isTextarea;
    this.isRequired = isRequired;
    this.skipValidation_ = skipValidation;
    this.isValidatable_ = false;
  }

  /**
   * Being validatable for an address component means that its invalid state
   * is visible to the user. Having a component not validatable initially
   * (before any interactions with controls) allows less aggressive validation
   * experience for the user.
   */
  get isValidatable(): boolean {
    return this.isValidatable_;
  }

  get isValid(): boolean {
    if (this.skipValidation_) {
      return true;
    }
    if (this.existingAddress_) {
      // "dont make it worse" validation for existing addresses:
      // consider a field valid as long as it is equal to the original value,
      // whether it is valid or not.
      if (this.originalValue_ === this.value ||
          (isValueEmpty(this.originalValue_) && isValueEmpty(this.value))) {
        return true;
      }
    }
    return !this.isRequired || !!this.value;
  }

  get value(): string|undefined {
    return this.addressFields_.get(this.fieldType_);
  }

  set value(value: string|undefined) {
    const changed = value !== this.value;
    this.addressFields_.set(this.fieldType_, value);
    if (changed) {
      this.onValueUpdateListener_();
    }
  }

  get hasValue(): boolean {
    return !isValueEmpty(this.value);
  }

  get fieldType(): chrome.autofillPrivate.FieldType {
    return this.fieldType_;
  }

  makeValidatable(): void {
    this.isValidatable_ = true;
  }
}