File

feature-libs/product-configurator/rulebased/components/attribute/product-card/configurator-attribute-product-card.component.ts

Extends

ConfiguratorAttributeBaseComponent

Implements

OnInit

Metadata

changeDetection ChangeDetectionStrategy.OnPush
selector cx-configurator-attribute-product-card
templateUrl ./configurator-attribute-product-card.component.html

Index

Properties
Methods
Inputs
Outputs
Accessors

Constructor

constructor(productService: ProductService, keyBoardFocus: KeyboardFocusService)
Parameters :
Name Type Optional
productService ProductService No
keyBoardFocus KeyboardFocusService No

Inputs

productCardOptions
Type : ConfiguratorAttributeProductCardComponentOptions

Outputs

handleDeselect
Type : EventEmitter
handleQuantity
Type : EventEmitter
handleSelect
Type : EventEmitter

Methods

extractPriceFormulaParameters
extractPriceFormulaParameters()

Extract corresponding price formula parameters

@return {ConfiguratorPriceComponentOptions} - New price formula

  • New price formula
extractQuantityParameters
extractQuantityParameters()

Extract corresponding quantity parameters

  • New quantity options
hasPriceDisplay
hasPriceDisplay()

Checks if price needs to be displayed. This is the case if either value price, quantity or value price total are present

Returns : boolean
  • Price display?
isProductCardSelected
isProductCardSelected()

Verifies whether the product card refers to a selected value

Returns : boolean
  • Selected?
isValueCodeDefined
isValueCodeDefined(valueCode: string | null | undefined)

Verifies whether the value code is defined.

Parameters :
Name Type Optional Description
valueCode string | null | undefined No
  • Value code
Returns : boolean
  • 'true' if the value code is defined, otherwise 'false'
ngOnInit
ngOnInit()
Returns : void
onChangeQuantity
onChangeQuantity(eventObject: any)
Parameters :
Name Type Optional
eventObject any No
Returns : void
onHandleDeselect
onHandleDeselect()
Returns : void
Protected onHandleQuantity
onHandleQuantity(quantity: number)
Parameters :
Name Type Optional
quantity number No
Returns : void
onHandleSelect
onHandleSelect()
Returns : void
showDeselectionNotPossibleMessage
showDeselectionNotPossibleMessage()
Returns : void
Protected transformToProductType
transformToProductType(value: Configurator.Value | undefined)
Parameters :
Name Type Optional
value Configurator.Value | undefined No
Returns : Product
createAriaLabelledBy
createAriaLabelledBy(prefix: string, attributeId: string, valueId?: string, hasQuantity?: boolean)

Creates unique key for attribute 'aria-labelledby'

Parameters :
Name Type Optional
prefix string No
attributeId string No
valueId string Yes
hasQuantity boolean Yes
Returns : string
createAttributeIdForConfigurator
createAttributeIdForConfigurator(currentAttribute: Configurator.Attribute)

Creates unique key for config attribute to be sent to configurator

Parameters :
Name Type Optional
currentAttribute Configurator.Attribute No
Returns : string
createAttributeUiKey
createAttributeUiKey(prefix: string, attributeId: string)

Creates unique key for config attribute on the UI

Parameters :
Name Type Optional Description
prefix string No

for key depending on usage (e.g. uiType, label)

attributeId string No
Returns : string
createAttributeValueIdForConfigurator
createAttributeValueIdForConfigurator(currentAttribute: Configurator.Attribute, value: string)

Creates unique key for config value to be sent to configurator

Parameters :
Name Type Optional
currentAttribute Configurator.Attribute No
value string No
Returns : string
createFocusId
createFocusId(attributeId: string, valueCode: string)

Creates a unique key for focus handling for the given attribute and value

Parameters :
Name Type Optional
attributeId string No
valueCode string No
Returns : string

focus key

createValueUiKey
createValueUiKey(prefix: string, attributeId: string, valueId: string)

Creates unique key for config value on the UI

Parameters :
Name Type Optional Description
prefix string No

for key depending on usage (e.g. uiType, label)

attributeId string No
valueId string No
Returns : string
Protected getAttributeCode
getAttributeCode(attribute: Configurator.Attribute)

Get code from attribute. The code is not a mandatory attribute (since not available for VC flavour), still it is mandatory in the context of CPQ. Calling this method therefore only makes sense when CPQ is active. In case the method is called in the wrong context, an exception will be thrown

Parameters :
Name Type Optional
attribute Configurator.Attribute No
Returns : number

Attribute code

Protected getUiType
getUiType(attribute: Configurator.Attribute)
Parameters :
Name Type Optional
attribute Configurator.Attribute No
Returns : string

Properties

handleDeselect
Default value : new EventEmitter<string>()
Decorators :
@Output()
handleQuantity
Default value : new EventEmitter<QuantityUpdateEvent>()
Decorators :
@Output()
handleSelect
Default value : new EventEmitter<string>()
Decorators :
@Output()
iconType
Default value : ICON_TYPE
loading$
Default value : new BehaviorSubject<boolean>(true)
product$
Type : Observable<Product>
productCardOptions
Type : ConfiguratorAttributeProductCardComponentOptions
Decorators :
@Input()
showDeselectionNotPossible
Default value : false
Private Static PREFIX
Type : string
Default value : 'cx-configurator'
Private Static PREFIX_DDLB_OPTION_PRICE_VALUE
Type : string
Default value : 'option--price'
Private Static PREFIX_LABEL
Type : string
Default value : 'label'
Private Static PREFIX_OPTION_PRICE_VALUE
Type : string
Default value : 'price--optionsPriceValue'
Private Static SEPERATOR
Type : string
Default value : '--'

Accessors

showQuantity
getshowQuantity()
focusConfig
getfocusConfig()
import {
  ChangeDetectionStrategy,
  Component,
  EventEmitter,
  Input,
  OnInit,
  Output,
} from '@angular/core';
import { Product, ProductService } from '@spartacus/core';
import { ConfiguratorProductScope } from '@spartacus/product-configurator/common';
import {
  FocusConfig,
  ICON_TYPE,
  KeyboardFocusService,
} from '@spartacus/storefront';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators';
import { Configurator } from '../../../core/model/configurator.model';
import { QuantityUpdateEvent } from '../../form/configurator-form.event';
import { ConfiguratorPriceComponentOptions } from '../../price/configurator-price.component';
import { ConfiguratorAttributeQuantityComponentOptions } from '../quantity/configurator-attribute-quantity.component';
import { ConfiguratorAttributeBaseComponent } from '../types/base/configurator-attribute-base.component';

export interface ConfiguratorAttributeProductCardComponentOptions {
  /** If set to `true`, all action buttons will be disabled.  */
  disableAllButtons?: boolean;
  /** If set to `true`, the remove/deselect button won't be available. Useful for required attributes,
   *  where a deselect/remove of last value shall not be possible.  */
  hideRemoveButton?: boolean;
  fallbackFocusId?: string;
  multiSelect?: boolean;
  productBoundValue: Configurator.Value;
  singleDropdown?: boolean;
  withQuantity?: boolean;
  /**
   * Used to indicate loading state, for example in case a request triggered by parent component to CPQ is currently in progress.
   * Component will react on it and disable all controls that could cause a request.
   * This prevents the user from triggering concurrent requests with potential conflicting content that might cause unexpected behaviour.
   */
  loading$?: Observable<boolean>;
  attributeId: number;
}

@Component({
  selector: 'cx-configurator-attribute-product-card',
  templateUrl: './configurator-attribute-product-card.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ConfiguratorAttributeProductCardComponent
  extends ConfiguratorAttributeBaseComponent
  implements OnInit
{
  product$: Observable<Product>;
  loading$ = new BehaviorSubject<boolean>(true);
  showDeselectionNotPossible = false;

  @Input()
  productCardOptions: ConfiguratorAttributeProductCardComponentOptions;

  @Output() handleDeselect = new EventEmitter<string>();
  @Output() handleQuantity = new EventEmitter<QuantityUpdateEvent>();
  @Output() handleSelect = new EventEmitter<string>();

  constructor(
    protected productService: ProductService,
    protected keyBoardFocus: KeyboardFocusService
  ) {
    super();
  }
  iconType = ICON_TYPE;

  ngOnInit() {
    this.loading$.next(true);
    const productSystemId =
      this.productCardOptions.productBoundValue.productSystemId;

    this.product$ = this.productService
      .get(
        productSystemId ? productSystemId : '',
        ConfiguratorProductScope.CONFIGURATOR_PRODUCT_CARD
      )
      .pipe(
        map((respProduct) => {
          return respProduct
            ? respProduct
            : this.transformToProductType(
                this.productCardOptions.productBoundValue
              );
        }),
        tap(() => this.loading$.next(false))
      );
  }

  get showQuantity(): boolean {
    return (
      (this.productCardOptions.withQuantity &&
        this.productCardOptions.productBoundValue.selected &&
        this.productCardOptions.multiSelect) ??
      false
    );
  }

  get focusConfig(): FocusConfig {
    const focusConfig = {
      key: this.createFocusId(
        this.productCardOptions.attributeId.toString(),
        this.productCardOptions.productBoundValue.valueCode
      ),
    };
    return focusConfig;
  }

  onHandleSelect(): void {
    this.loading$.next(true);
    if (
      this.productCardOptions.hideRemoveButton &&
      this.productCardOptions.fallbackFocusId
    ) {
      this.keyBoardFocus.set(this.productCardOptions.fallbackFocusId);
    }
    this.handleSelect.emit(this.productCardOptions.productBoundValue.valueCode);
  }

  onHandleDeselect(): void {
    {
      if (
        this.productCardOptions.productBoundValue.selected &&
        this.productCardOptions.hideRemoveButton
      ) {
        this.showDeselectionNotPossibleMessage();
        return;
      }
      this.loading$.next(true);
      this.handleDeselect.emit(
        this.productCardOptions.productBoundValue.valueCode
      );
    }
  }

  onChangeQuantity(eventObject: any): void {
    if (!eventObject) {
      this.onHandleDeselect();
    } else {
      this.onHandleQuantity(eventObject);
    }
  }

  /**
   * Verifies whether the product card refers to a selected value
   * @return {boolean} - Selected?
   */
  isProductCardSelected(): boolean {
    const isProductCardSelected =
      this.productCardOptions.productBoundValue &&
      this.productCardOptions.productBoundValue.selected &&
      !this.productCardOptions.singleDropdown;

    return isProductCardSelected ?? false;
  }

  /**
   * Checks if price needs to be displayed. This is the
   * case if either value price, quantity or value price total
   * are present
   * @return {boolean} - Price display?
   */
  hasPriceDisplay(): boolean {
    const productPrice =
      this.productCardOptions.productBoundValue.valuePrice ||
      this.productCardOptions.productBoundValue.quantity ||
      this.productCardOptions.productBoundValue.valuePriceTotal;

    return productPrice ? true : false;
  }

  /**
   * Extract corresponding price formula parameters
   *
   *  @return {ConfiguratorPriceComponentOptions} - New price formula
   */
  extractPriceFormulaParameters(): ConfiguratorPriceComponentOptions {
    if (!this.productCardOptions.multiSelect) {
      return {
        price: this.productCardOptions.productBoundValue.valuePrice,
        isLightedUp: this.productCardOptions.productBoundValue.selected,
      };
    }
    return {
      quantity: this.productCardOptions.productBoundValue.quantity,
      price: this.productCardOptions.productBoundValue.valuePrice,
      priceTotal: this.productCardOptions.productBoundValue.valuePriceTotal,
      isLightedUp: this.productCardOptions.productBoundValue.selected,
    };
  }

  /**
   *  Extract corresponding quantity parameters
   *
   * @return {ConfiguratorAttributeQuantityComponentOptions} - New quantity options
   */
  extractQuantityParameters(): ConfiguratorAttributeQuantityComponentOptions {
    const quantityFromOptions =
      this.productCardOptions.productBoundValue.quantity;

    const mergedLoading = this.productCardOptions.loading$
      ? combineLatest([this.loading$, this.productCardOptions.loading$]).pipe(
          map((values) => {
            return values[0] || values[1];
          })
        )
      : this.loading$;

    return {
      allowZero: !this.productCardOptions.hideRemoveButton,
      initialQuantity: quantityFromOptions ? quantityFromOptions : 0,
      disableQuantityActions$: mergedLoading,
    };
  }

  /**
   * Verifies whether the value code is defined.
   *
   * @param {string} valueCode - Value code
   * @return {boolean} - 'true' if the value code is defined, otherwise 'false'
   */
  isValueCodeDefined(valueCode: string | null | undefined): boolean {
    return valueCode && valueCode !== '0' ? true : false;
  }

  protected transformToProductType(
    value: Configurator.Value | undefined
  ): Product {
    return {
      code: value?.productSystemId,
      description: value?.description,
      images: {},
      name: value?.valueDisplay,
    };
  }

  protected onHandleQuantity(quantity: number): void {
    this.loading$.next(true);

    this.handleQuantity.emit({
      quantity,
      valueCode: this.productCardOptions.productBoundValue.valueCode,
    });
  }

  showDeselectionNotPossibleMessage() {
    this.showDeselectionNotPossible = true;
  }
}
<ng-container *ngIf="product$ | async as product">
  <div
    class="cx-product-card"
    [ngClass]="{
      'cx-product-card-selected': isProductCardSelected()
    }"
  >
    <div class="cx-product-card-rows">
      <div class="cx-product-card-imgs">
        <cx-media
          [container]="product?.images?.PRIMARY"
          format="product"
        ></cx-media>
      </div>

      <div class="cx-product-card-info">
        <div class="cx-product-card-name">
          <p>
            {{ product.name }}
          </p>
        </div>
        <div class="cx-product-card-code" *ngIf="product.code">
          {{ 'configurator.attribute.id' | cxTranslate }}:
          {{ product.code }}
        </div>
        <cx-configurator-show-more
          *ngIf="product?.description"
          [text]="product?.description"
          [textSize]="45"
        ></cx-configurator-show-more>
      </div>
    </div>

    <div
      class="cx-product-card-rows column"
      *ngIf="!productCardOptions.singleDropdown || hasPriceDisplay()"
    >
      <div class="cx-product-card-quantity-price">
        <div class="cx-product-card-quantity">
          <cx-configurator-attribute-quantity
            *ngIf="showQuantity"
            (changeQuantity)="onChangeQuantity($event)"
            [quantityOptions]="extractQuantityParameters()"
          ></cx-configurator-attribute-quantity>
        </div>
        <div class="cx-product-card-price">
          <cx-configurator-price
            [formula]="extractPriceFormulaParameters()"
          ></cx-configurator-price>
        </div>
      </div>
      <div class="cx-product-card-action">
        <div
          class="cx-product-card-action-btn"
          *ngIf="!productCardOptions?.singleDropdown"
        >
          <ng-container *ngIf="productCardOptions?.multiSelect; else single">
            <button
              *ngIf="
                productCardOptions?.productBoundValue?.selected;
                else select
              "
              class="btn btn-action"
              (click)="onHandleDeselect()"
              [cxFocus]="focusConfig"
            >
              {{ 'configurator.button.remove' | cxTranslate }}
            </button>

            <ng-template #select>
              <button
                class="btn btn-primary"
                (click)="onHandleSelect()"
                [disabled]="
                  productCardOptions?.disableAllButtons || (loading$ | async)
                "
                [cxFocus]="focusConfig"
              >
                {{ 'configurator.button.add' | cxTranslate }}
              </button>
            </ng-template>
          </ng-container>

          <ng-template #single>
            <button
              class="btn btn-primary"
              (click)="onHandleSelect()"
              [disabled]="
                productCardOptions?.disableAllButtons || (loading$ | async)
              "
              *ngIf="
                !productCardOptions?.productBoundValue?.selected;
                else deselect
              "
              [cxFocus]="focusConfig"
            >
              {{ 'configurator.button.select' | cxTranslate }}
            </button>
            <ng-template #deselect>
              <ng-container
                *ngIf="
                  isValueCodeDefined(
                    productCardOptions?.productBoundValue?.valueCode
                  )
                "
              >
                <button
                  *ngIf="!productCardOptions?.hideRemoveButton"
                  class="btn btn-action"
                  (click)="onHandleDeselect()"
                  [disabled]="
                    productCardOptions?.hideRemoveButton || (loading$ | async)
                  "
                  [cxFocus]="focusConfig"
                >
                  {{ 'configurator.button.deselect' | cxTranslate }}
                </button>
              </ng-container>
            </ng-template>
          </ng-template>
        </div>
      </div>
    </div>
    <ng-container *ngIf="showDeselectionNotPossible">
      <div class="cx-product-card-rows deselection-error-message">
        <cx-icon class="deselection-error-symbol" type="ERROR"></cx-icon>
        {{ 'configurator.attribute.deselectionNotPossible' | cxTranslate }}
      </div>
    </ng-container>
  </div>
</ng-container>
Legend
Html element
Component
Html element with directive

result-matching ""

    No results matching ""