feature-libs/product-configurator/rulebased/components/attribute/product-card/configurator-attribute-product-card.component.ts
ConfiguratorAttributeBaseComponent
changeDetection | ChangeDetectionStrategy.OnPush |
selector | cx-configurator-attribute-product-card |
templateUrl | ./configurator-attribute-product-card.component.html |
Properties |
|
Methods |
|
Inputs |
Outputs |
Accessors |
constructor(productService: ProductService, keyBoardFocus: KeyboardFocusService)
|
|||||||||
Parameters :
|
productCardOptions | |
Type : ConfiguratorAttributeProductCardComponentOptions
|
|
handleDeselect | |
Type : EventEmitter
|
|
handleQuantity | |
Type : EventEmitter
|
|
handleSelect | |
Type : EventEmitter
|
|
extractPriceFormulaParameters |
extractPriceFormulaParameters()
|
Extract corresponding price formula parameters @return {ConfiguratorPriceComponentOptions} - New price formula
Returns :
ConfiguratorPriceComponentOptions
|
extractQuantityParameters |
extractQuantityParameters()
|
Extract corresponding quantity parameters
|
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
|
isProductCardSelected |
isProductCardSelected()
|
Verifies whether the product card refers to a selected value
Returns :
boolean
|
isValueCodeDefined | ||||||||
isValueCodeDefined(valueCode: string | null | undefined)
|
||||||||
Verifies whether the value code is defined.
Parameters :
Returns :
boolean
|
ngOnInit |
ngOnInit()
|
Returns :
void
|
onChangeQuantity | ||||||
onChangeQuantity(eventObject: any)
|
||||||
Parameters :
Returns :
void
|
onHandleDeselect |
onHandleDeselect()
|
Returns :
void
|
Protected onHandleQuantity | ||||||
onHandleQuantity(quantity: number)
|
||||||
Parameters :
Returns :
void
|
onHandleSelect |
onHandleSelect()
|
Returns :
void
|
showDeselectionNotPossibleMessage |
showDeselectionNotPossibleMessage()
|
Returns :
void
|
Protected transformToProductType | ||||||
transformToProductType(value: Configurator.Value | undefined)
|
||||||
Parameters :
Returns :
Product
|
createAriaLabelledBy | |||||||||||||||
createAriaLabelledBy(prefix: string, attributeId: string, valueId?: string, hasQuantity?: boolean)
|
|||||||||||||||
Inherited from
ConfiguratorAttributeBaseComponent
|
|||||||||||||||
Defined in
ConfiguratorAttributeBaseComponent:89
|
|||||||||||||||
Creates unique key for attribute 'aria-labelledby'
Parameters :
Returns :
string
|
createAttributeIdForConfigurator | ||||||
createAttributeIdForConfigurator(currentAttribute: Configurator.Attribute)
|
||||||
Inherited from
ConfiguratorAttributeBaseComponent
|
||||||
Defined in
ConfiguratorAttributeBaseComponent:73
|
||||||
Creates unique key for config attribute to be sent to configurator
Parameters :
Returns :
string
|
createAttributeUiKey | ||||||||||||
createAttributeUiKey(prefix: string, attributeId: string)
|
||||||||||||
Inherited from
ConfiguratorAttributeBaseComponent
|
||||||||||||
Defined in
ConfiguratorAttributeBaseComponent:59
|
||||||||||||
Creates unique key for config attribute on the UI
Parameters :
Returns :
string
|
createAttributeValueIdForConfigurator | |||||||||
createAttributeValueIdForConfigurator(currentAttribute: Configurator.Attribute, value: string)
|
|||||||||
Inherited from
ConfiguratorAttributeBaseComponent
|
|||||||||
Defined in
ConfiguratorAttributeBaseComponent:37
|
|||||||||
Creates unique key for config value to be sent to configurator
Parameters :
Returns :
string
|
createFocusId |
createFocusId(attributeId: string, valueCode: string)
|
Inherited from
ConfiguratorAttributeBaseComponent
|
Defined in
ConfiguratorAttributeBaseComponent:133
|
Creates a unique key for focus handling for the given attribute and value
Returns :
string
focus key |
createValueUiKey | ||||||||||||||||
createValueUiKey(prefix: string, attributeId: string, valueId: string)
|
||||||||||||||||
Inherited from
ConfiguratorAttributeBaseComponent
|
||||||||||||||||
Defined in
ConfiguratorAttributeBaseComponent:20
|
||||||||||||||||
Creates unique key for config value on the UI
Parameters :
Returns :
string
|
Protected getAttributeCode | ||||||
getAttributeCode(attribute: Configurator.Attribute)
|
||||||
Inherited from
ConfiguratorAttributeBaseComponent
|
||||||
Defined in
ConfiguratorAttributeBaseComponent:147
|
||||||
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 :
Returns :
number
Attribute code |
Protected getUiType | ||||||
getUiType(attribute: Configurator.Attribute)
|
||||||
Inherited from
ConfiguratorAttributeBaseComponent
|
||||||
Defined in
ConfiguratorAttributeBaseComponent:48
|
||||||
Parameters :
Returns :
string
|
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'
|
Inherited from
ConfiguratorAttributeBaseComponent
|
Defined in
ConfiguratorAttributeBaseComponent:9
|
Private Static PREFIX_DDLB_OPTION_PRICE_VALUE |
Type : string
|
Default value : 'option--price'
|
Inherited from
ConfiguratorAttributeBaseComponent
|
Defined in
ConfiguratorAttributeBaseComponent:12
|
Private Static PREFIX_LABEL |
Type : string
|
Default value : 'label'
|
Inherited from
ConfiguratorAttributeBaseComponent
|
Defined in
ConfiguratorAttributeBaseComponent:10
|
Private Static PREFIX_OPTION_PRICE_VALUE |
Type : string
|
Default value : 'price--optionsPriceValue'
|
Inherited from
ConfiguratorAttributeBaseComponent
|
Defined in
ConfiguratorAttributeBaseComponent:11
|
Private Static SEPERATOR |
Type : string
|
Default value : '--'
|
Inherited from
ConfiguratorAttributeBaseComponent
|
Defined in
ConfiguratorAttributeBaseComponent:8
|
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>