feature-libs/cart/quick-order/components/quick-order/form/quick-order-form.component.ts
| changeDetection | ChangeDetectionStrategy.OnPush |
| selector | cx-quick-order-form |
| templateUrl | ./quick-order-form.component.html |
Properties |
|
Methods |
|
constructor(globalMessageService: GlobalMessageService, quickOrderService: QuickOrderFacade, config?: Config, cd?: ChangeDetectorRef, winRef?: WindowRef)
|
||||||||||||||||||
|
Parameters :
|
| add |
add(product: Product, event: Event)
|
|
Returns :
void
|
| addProduct | ||||||
addProduct(event: Event)
|
||||||
|
Parameters :
Returns :
void
|
| Protected blurSuggestionBox | ||||||
blurSuggestionBox(event: UIEvent)
|
||||||
|
Parameters :
Returns :
void
|
| Protected buildForm |
buildForm()
|
|
Returns :
void
|
| canAddProduct |
canAddProduct()
|
|
Returns :
Observable<boolean>
|
| clear | ||||||
clear(event?: Event)
|
||||||
|
Parameters :
Returns :
void
|
| Protected clearResults |
clearResults()
|
|
Returns :
void
|
| Protected close |
close()
|
|
Returns :
void
|
| focusNextChild | ||||||
focusNextChild(event: UIEvent)
|
||||||
|
Parameters :
Returns :
void
|
| focusPreviousChild | ||||||
focusPreviousChild(event: UIEvent)
|
||||||
|
Parameters :
Returns :
void
|
| Protected getFocusedElement |
getFocusedElement()
|
|
Returns :
HTMLElement | any
|
| Protected getFocusedIndex |
getFocusedIndex()
|
|
Returns :
number
|
| Protected getResultElements |
getResultElements()
|
|
Returns :
HTMLElement[]
|
| Protected isEmpty | ||||||
isEmpty(string?: string)
|
||||||
|
Parameters :
Returns :
boolean
|
| isResultsBoxOpen |
isResultsBoxOpen()
|
|
Returns :
boolean
|
| Protected isSuggestionFocused |
isSuggestionFocused()
|
|
Returns :
boolean
|
| ngOnDestroy |
ngOnDestroy()
|
|
Returns :
void
|
| ngOnInit |
ngOnInit()
|
|
Returns :
void
|
| onBlur | ||||||
onBlur(event: UIEvent)
|
||||||
|
Parameters :
Returns :
void
|
| open |
open()
|
|
Returns :
void
|
| Protected resetSearchSubscription |
resetSearchSubscription()
|
|
Returns :
void
|
| Protected searchProducts | ||||||
searchProducts(query: string)
|
||||||
|
Parameters :
Returns :
void
|
| Protected toggleBodyClass |
toggleBodyClass(className: string, add?: boolean)
|
|
Returns :
void
|
| Protected watchProductAdd |
watchProductAdd()
|
|
Returns :
Subscription
|
| Protected watchQueryChange |
watchQueryChange()
|
|
Returns :
Subscription
|
| Public Optional config |
Type : Config
|
| form |
Type : FormGroup
|
| iconTypes |
Default value : ICON_TYPE
|
| isSearching |
Type : boolean
|
Default value : false
|
| noResults |
Type : boolean
|
Default value : false
|
| results |
Type : Product[]
|
Default value : []
|
| Protected searchSubscription |
Default value : new Subscription()
|
| Protected subscription |
Default value : new Subscription()
|
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
OnDestroy,
OnInit,
} from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { QuickOrderFacade } from '@spartacus/cart/quick-order/root';
import {
Config,
GlobalMessageService,
Product,
WindowRef,
} from '@spartacus/core';
import { ICON_TYPE } from '@spartacus/storefront';
import { Observable, Subscription } from 'rxjs';
import {
debounceTime,
distinctUntilChanged,
filter,
switchMap,
take,
} from 'rxjs/operators';
@Component({
selector: 'cx-quick-order-form',
templateUrl: './quick-order-form.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class QuickOrderFormComponent implements OnInit, OnDestroy {
form: FormGroup;
iconTypes = ICON_TYPE;
isSearching: boolean = false;
noResults: boolean = false;
results: Product[] = [];
protected subscription = new Subscription();
protected searchSubscription = new Subscription();
/**
* @deprecated since version 4.2
* Use constructor(globalMessageService: GlobalMessageService, quickOrderService: QuickOrderFacade, config: Config, cd: ChangeDetectorRef, winRef: WindowRef); instead
*/
// TODO(#14058): Remove deprecated constructor
constructor(
globalMessageService: GlobalMessageService,
quickOrderService: QuickOrderFacade
);
constructor(
protected globalMessageService: GlobalMessageService, // TODO(#14058): Remove it as it is not in use anymore
protected quickOrderService: QuickOrderFacade,
public config?: Config, // TODO(#14058): Make it required
protected cd?: ChangeDetectorRef, // TODO(#14058): Make it required
protected winRef?: WindowRef // TODO(#14058): Make it required
) {}
ngOnInit(): void {
this.buildForm();
this.subscription.add(this.watchProductAdd());
this.subscription.add(this.watchQueryChange());
}
onBlur(event: UIEvent): void {
// Use timeout to detect changes
setTimeout(() => {
if (!this.isSuggestionFocused()) {
this.blurSuggestionBox(event);
}
});
}
clear(event?: Event): void {
event?.preventDefault();
if (this.isResultsBoxOpen()) {
this.toggleBodyClass('quick-order-searchbox-is-active', false);
}
let product = this.form.get('product')?.value;
if (!!product) {
this.form.reset();
}
// We have to call 'close' method every time to make sure results list is empty and call detectChanges to change icon type in form
this.close();
this.cd?.detectChanges();
}
add(product: Product, event: Event): void {
event?.preventDefault();
// TODO change to nonpurchasable flag once we will support multidimensional products in search and when the purchasable flag will be available in search product response
// Check if product is purchasable / non multidimensional
if (product.multidimensional) {
this.quickOrderService.setNonPurchasableProductError(product);
this.clear();
return;
} else {
this.quickOrderService.clearNonPurchasableProductError();
}
this.quickOrderService.addProduct(product);
}
addProduct(event: Event): void {
this.quickOrderService
.canAdd()
.pipe(take(1))
.subscribe((canAdd: boolean) => {
if (canAdd) {
// Add product if there is only one in the result list
if (this.results.length === 1) {
this.add(this.results[0], event);
// Add product if there is focus on it
} else if (this.getFocusedIndex() !== -1) {
const product = this.results[this.getFocusedIndex()];
this.add(product, event);
}
}
});
}
focusNextChild(event: UIEvent): void {
event.preventDefault(); // Negate normal keyscroll
if (!this.results.length) {
return;
}
const [results, focusedIndex] = [
this.getResultElements(),
this.getFocusedIndex(),
];
// Focus on first index moving to last
if (results.length) {
if (focusedIndex >= results.length - 1) {
results[0].focus();
} else {
results[focusedIndex + 1].focus();
}
}
}
focusPreviousChild(event: UIEvent): void {
event.preventDefault(); // Negate normal keyscroll
if (!this.results.length) {
return;
}
const [results, focusedIndex] = [
this.getResultElements(),
this.getFocusedIndex(),
];
// Focus on last index moving to first
if (results.length) {
if (focusedIndex < 1) {
results[results.length - 1].focus();
} else {
results[focusedIndex - 1].focus();
}
}
}
isResultsBoxOpen(): boolean {
return this.winRef
? !!this.winRef.document.querySelector('.quick-order-searchbox-is-active')
: false;
}
canAddProduct(): Observable<boolean> {
return this.quickOrderService.canAdd();
}
open(): void {
this.toggleBodyClass('quick-order-searchbox-is-active', true);
}
// Return result list as HTMLElement array
protected getResultElements(): HTMLElement[] {
if (this.winRef) {
return Array.from(
this.winRef.document.querySelectorAll(
'.quick-order-results-products > li button'
)
);
} else {
return [];
}
}
protected blurSuggestionBox(event: UIEvent): void {
this.toggleBodyClass('quick-order-searchbox-is-active', false);
if (event && event.target) {
(<HTMLElement>event.target).blur();
}
}
// Return focused element as HTMLElement
protected getFocusedElement(): HTMLElement | any {
if (this.winRef) {
return <HTMLElement>this.winRef.document.activeElement;
}
}
protected getFocusedIndex(): number {
return this.getResultElements().indexOf(this.getFocusedElement());
}
protected isSuggestionFocused(): boolean {
return this.getResultElements().includes(this.getFocusedElement());
}
protected toggleBodyClass(className: string, add?: boolean) {
// TODO(#14058): Remove condition
if (this.winRef) {
if (add === undefined) {
this.winRef.document.body.classList.toggle(className);
} else {
add
? this.winRef.document.body.classList.add(className)
: this.winRef.document.body.classList.remove(className);
}
}
}
protected buildForm() {
const form = new FormGroup({});
form.setControl('product', new FormControl(null));
this.form = form;
}
protected isEmpty(string?: string): boolean {
return string?.trim() === '' || string == null;
}
protected watchQueryChange(): Subscription {
return this.form.valueChanges
.pipe(
distinctUntilChanged(),
debounceTime(300),
filter((value) => {
if (this.config?.quickOrder?.searchForm) {
//Check if input to quick order is an empty after deleting input manually
if (this.isEmpty(value.product)) {
//Clear recommendation results on empty string
this.clear();
return false;
}
return (
!!value.product &&
value.product.length >=
this.config.quickOrder?.searchForm?.minCharactersBeforeRequest
);
}
return value;
})
)
.subscribe((value) => {
this.searchProducts(value.product);
});
}
protected searchProducts(query: string): void {
this.searchSubscription.add(
this.canAddProduct()
.pipe(
filter(Boolean),
switchMap(() =>
this.quickOrderService
.searchProducts(
query,
this.config?.quickOrder?.searchForm?.maxProducts
)
.pipe(take(1))
)
)
.subscribe((products) => {
this.results = products;
if (this.results.length) {
this.noResults = false;
this.open();
} else {
this.noResults = true;
}
this.cd?.detectChanges();
})
);
}
protected clearResults(): void {
this.results = [];
}
protected close(): void {
this.resetSearchSubscription();
this.clearResults();
this.noResults = false;
}
protected resetSearchSubscription(): void {
this.searchSubscription.unsubscribe();
this.searchSubscription = new Subscription();
}
protected watchProductAdd(): Subscription {
return this.quickOrderService
.getProductAdded()
.subscribe(() => this.clear());
}
ngOnDestroy(): void {
this.subscription.unsubscribe();
}
}
<form [formGroup]="form" class="quick-order-form-container">
<div class="quick-order-form-input">
<input
(blur)="onBlur($event)"
(focus)="open()"
(keydown.arrowdown)="focusNextChild($event)"
(keydown.arrowup)="focusPreviousChild($event)"
(keydown.enter)="addProduct($event)"
(keydown.escape)="clear($event)"
[attr.aria-label]="'common.search' | cxTranslate"
class="form-control"
formControlName="product"
placeholder="{{ 'quickOrderForm.placeholder' | cxTranslate }}"
type="text"
/>
<button
*ngIf="form.get('product')?.value; else searchIcon"
(click)="clear($event)"
(keydown.enter)="clear($event)"
[attr.aria-label]="'common.reset' | cxTranslate"
class="quick-order-form-reset-icon"
>
<cx-icon [type]="iconTypes.RESET"></cx-icon>
</button>
<ng-template #searchIcon>
<button
[attr.aria-label]="'common.search' | cxTranslate"
class="quick-order-form-search-icon"
tabindex="-1"
>
<cx-icon [type]="iconTypes.SEARCH"></cx-icon>
</button>
</ng-template>
<span
*ngIf="!(canAddProduct() | async) && form.get('product')?.dirty"
class="list-limit-reached-text"
>
{{ 'quickOrderForm.listLimitReached' | cxTranslate }}
</span>
</div>
<div *ngIf="isResultsBoxOpen()" role="listbox" class="quick-order-results">
<ul *ngIf="results.length" class="quick-order-results-products">
<li
*ngFor="let product of results; let i = index"
class="quick-order-results-product-container"
>
<button
(blur)="onBlur($event)"
(click)="add(product, $event)"
(keydown.arrowdown)="focusNextChild($event)"
(keydown.arrowup)="focusPreviousChild($event)"
(keydown.enter)="add(product, $event)"
(keydown.escape)="clear($event)"
[attr.aria-label]="
'quickOrderForm.addProduct' | cxTranslate: { product: product.name }
"
[class.has-media]="
config?.quickOrder?.searchForm?.displayProductImages
"
class="quick-order-results-product"
>
<cx-media
*ngIf="config?.quickOrder?.searchForm?.displayProductImages"
[container]="product.images?.PRIMARY"
class="media"
format="thumbnail"
role="presentation"
></cx-media>
<div class="name" [innerHTML]="product.name"></div>
<span class="id">
{{
'quickOrderForm.id'
| cxTranslate
: {
id: product.code
}
}}
</span>
<span class="price">{{ product.price?.formattedValue }}</span>
</button>
</li>
</ul>
<span *ngIf="noResults" class="quick-order-no-results">
{{ 'quickOrderForm.noResults' | cxTranslate }}
</span>
</div>
</form>