File

feature-libs/cart/quick-order/components/quick-order/form/quick-order-form.component.ts

Implements

OnInit OnDestroy

Metadata

changeDetection ChangeDetectionStrategy.OnPush
selector cx-quick-order-form
templateUrl ./quick-order-form.component.html

Index

Properties
Methods

Constructor

constructor(globalMessageService: GlobalMessageService, quickOrderService: QuickOrderFacade, config?: Config, cd?: ChangeDetectorRef, winRef?: WindowRef)
Parameters :
Name Type Optional
globalMessageService GlobalMessageService No
quickOrderService QuickOrderFacade No
config Config Yes
cd ChangeDetectorRef Yes
winRef WindowRef Yes

Methods

add
add(product: Product, event: Event)
Parameters :
Name Type Optional
product Product No
event Event No
Returns : void
addProduct
addProduct(event: Event)
Parameters :
Name Type Optional
event Event No
Returns : void
Protected blurSuggestionBox
blurSuggestionBox(event: UIEvent)
Parameters :
Name Type Optional
event UIEvent No
Returns : void
Protected buildForm
buildForm()
Returns : void
canAddProduct
canAddProduct()
Returns : Observable<boolean>
clear
clear(event?: Event)
Parameters :
Name Type Optional
event Event Yes
Returns : void
Protected clearResults
clearResults()
Returns : void
Protected close
close()
Returns : void
focusNextChild
focusNextChild(event: UIEvent)
Parameters :
Name Type Optional
event UIEvent No
Returns : void
focusPreviousChild
focusPreviousChild(event: UIEvent)
Parameters :
Name Type Optional
event UIEvent No
Returns : void
Protected getFocusedElement
getFocusedElement()
Returns : HTMLElement | any
Protected getFocusedIndex
getFocusedIndex()
Returns : number
Protected getResultElements
getResultElements()
Returns : HTMLElement[]
Protected isEmpty
isEmpty(string?: string)
Parameters :
Name Type Optional
string string Yes
Returns : boolean
isResultsBoxOpen
isResultsBoxOpen()
Returns : boolean
Protected isSuggestionFocused
isSuggestionFocused()
Returns : boolean
ngOnDestroy
ngOnDestroy()
Returns : void
ngOnInit
ngOnInit()
Returns : void
onBlur
onBlur(event: UIEvent)
Parameters :
Name Type Optional
event UIEvent No
Returns : void
open
open()
Returns : void
Protected resetSearchSubscription
resetSearchSubscription()
Returns : void
Protected searchProducts
searchProducts(query: string)
Parameters :
Name Type Optional
query string No
Returns : void
Protected toggleBodyClass
toggleBodyClass(className: string, add?: boolean)
Parameters :
Name Type Optional
className string No
add boolean Yes
Returns : void
Protected watchProductAdd
watchProductAdd()
Returns : Subscription
Protected watchQueryChange
watchQueryChange()
Returns : Subscription

Properties

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>
Legend
Html element
Component
Html element with directive

result-matching ""

    No results matching ""