Skip to content

Commit

Permalink
feat(Commerce Atomic): add product price component (#3918)
Browse files Browse the repository at this point in the history
This PR adds the product price template component. This component
displays the value of ec_price, in the specified currency. Additionally,
it displays the value of ec_promo_price when that value is lower.

<img width="1000" alt="image"
src="https://github.com/coveo/ui-kit/assets/1728218/682cd999-7b50-41fd-b9da-c28541551cc3">

---------

Co-authored-by: Frederic Beaudoin <fbeaudoin@coveo.com>
  • Loading branch information
fpbrault and fbeaudoincoveo authored May 9, 2024
1 parent a5da5ce commit 67c9d80
Show file tree
Hide file tree
Showing 8 changed files with 210 additions and 48 deletions.
42 changes: 42 additions & 0 deletions packages/atomic/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1570,6 +1570,18 @@ export namespace Components {
*/
"hrefTemplate"?: string;
}
interface AtomicProductNumericFieldValue {
/**
* The field that the component should use. The component will try to find this field in the `Product.additionalFields` object unless it finds it in the `Product` object first.
*/
"field": string;
}
interface AtomicProductPrice {
/**
* The currency to use in currency formatting. Allowed values are the [ISO 4217 currency codes](https://www.six-group.com/en/products-services/financial-information/data-standards.html#scrollTo=maintenance-agency), such as "USD" for the US dollar, "EUR" for the euro, or "CNY" for the Chinese RMB.
*/
"currency": string;
}
interface AtomicProductTemplate {
/**
* A function that must return true on products for the product template to apply. Set programmatically before initialization, not via attribute. For example, the following targets a template and sets a condition to make it apply only to products whose `ec_name` contains `singapore`: `document.querySelector('#target-template').conditions = [(product) => /singapore/i.test(product.ec_name)];`
Expand Down Expand Up @@ -3828,6 +3840,18 @@ declare global {
prototype: HTMLAtomicProductLinkElement;
new (): HTMLAtomicProductLinkElement;
};
interface HTMLAtomicProductNumericFieldValueElement extends Components.AtomicProductNumericFieldValue, HTMLStencilElement {
}
var HTMLAtomicProductNumericFieldValueElement: {
prototype: HTMLAtomicProductNumericFieldValueElement;
new (): HTMLAtomicProductNumericFieldValueElement;
};
interface HTMLAtomicProductPriceElement extends Components.AtomicProductPrice, HTMLStencilElement {
}
var HTMLAtomicProductPriceElement: {
prototype: HTMLAtomicProductPriceElement;
new (): HTMLAtomicProductPriceElement;
};
interface HTMLAtomicProductTemplateElement extends Components.AtomicProductTemplate, HTMLStencilElement {
}
var HTMLAtomicProductTemplateElement: {
Expand Down Expand Up @@ -4754,6 +4778,8 @@ declare global {
"atomic-product": HTMLAtomicProductElement;
"atomic-product-description": HTMLAtomicProductDescriptionElement;
"atomic-product-link": HTMLAtomicProductLinkElement;
"atomic-product-numeric-field-value": HTMLAtomicProductNumericFieldValueElement;
"atomic-product-price": HTMLAtomicProductPriceElement;
"atomic-product-template": HTMLAtomicProductTemplateElement;
"atomic-product-text": HTMLAtomicProductTextElement;
"atomic-query-error": HTMLAtomicQueryErrorElement;
Expand Down Expand Up @@ -6277,6 +6303,18 @@ declare namespace LocalJSX {
*/
"hrefTemplate"?: string;
}
interface AtomicProductNumericFieldValue {
/**
* The field that the component should use. The component will try to find this field in the `Product.additionalFields` object unless it finds it in the `Product` object first.
*/
"field": string;
}
interface AtomicProductPrice {
/**
* The currency to use in currency formatting. Allowed values are the [ISO 4217 currency codes](https://www.six-group.com/en/products-services/financial-information/data-standards.html#scrollTo=maintenance-agency), such as "USD" for the US dollar, "EUR" for the euro, or "CNY" for the Chinese RMB.
*/
"currency"?: string;
}
interface AtomicProductTemplate {
/**
* A function that must return true on products for the product template to apply. Set programmatically before initialization, not via attribute. For example, the following targets a template and sets a condition to make it apply only to products whose `ec_name` contains `singapore`: `document.querySelector('#target-template').conditions = [(product) => /singapore/i.test(product.ec_name)];`
Expand Down Expand Up @@ -7647,6 +7685,8 @@ declare namespace LocalJSX {
"atomic-product": AtomicProduct;
"atomic-product-description": AtomicProductDescription;
"atomic-product-link": AtomicProductLink;
"atomic-product-numeric-field-value": AtomicProductNumericFieldValue;
"atomic-product-price": AtomicProductPrice;
"atomic-product-template": AtomicProductTemplate;
"atomic-product-text": AtomicProductText;
"atomic-query-error": AtomicQueryError;
Expand Down Expand Up @@ -7963,6 +8003,8 @@ declare module "@stencil/core" {
"atomic-product": LocalJSX.AtomicProduct & JSXBase.HTMLAttributes<HTMLAtomicProductElement>;
"atomic-product-description": LocalJSX.AtomicProductDescription & JSXBase.HTMLAttributes<HTMLAtomicProductDescriptionElement>;
"atomic-product-link": LocalJSX.AtomicProductLink & JSXBase.HTMLAttributes<HTMLAtomicProductLinkElement>;
"atomic-product-numeric-field-value": LocalJSX.AtomicProductNumericFieldValue & JSXBase.HTMLAttributes<HTMLAtomicProductNumericFieldValueElement>;
"atomic-product-price": LocalJSX.AtomicProductPrice & JSXBase.HTMLAttributes<HTMLAtomicProductPriceElement>;
"atomic-product-template": LocalJSX.AtomicProductTemplate & JSXBase.HTMLAttributes<HTMLAtomicProductTemplateElement>;
"atomic-product-text": LocalJSX.AtomicProductText & JSXBase.HTMLAttributes<HTMLAtomicProductTextElement>;
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,23 @@ import {
TemplateProviderProps,
} from '../../common/template-provider/template-provider';

// TODO: Add JSX support for default template
function defaultTemplate() {
const content = document.createDocumentFragment();
const linkEl = document.createElement('atomic-product-link');
const descEl = document.createElement('atomic-product-description');
const imgEl = document.createElement('atomic-product-image');
imgEl.setAttribute('field', 'ec_thumbnails');
content.appendChild(linkEl);
content.appendChild(imgEl);
content.appendChild(descEl);

const markup = `
<atomic-product-link class="font-bold"></atomic-product-link>
<atomic-product-text field="ec_brand" class="block text-neutral-dark"></atomic-product-text>
<atomic-product-image field="ec_thumbnails"></atomic-product-image>
<atomic-product-rating field="ec_rating"></atomic-product-rating>
<atomic-product-price currency="USD" class="text-2xl"></atomic-product-price>
<atomic-product-description></atomic-product-description>
`;

const template = document.createElement('template');
template.innerHTML = markup.trim();
content.appendChild(template.content);

return {
content,
conditions: [],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import {Product, ProductTemplatesHelpers} from '@coveo/headless/commerce';
import {Component, Prop, Element, State, Listen} from '@stencil/core';
import {Bindings} from '../../../../components';
import {InitializeBindings} from '../../../../utils/initialization-utils';
import {
defaultNumberFormatter,
NumberFormatter,
} from '../../../common/formats/format-common';
import {ProductContext} from '../product-template-decorators';

/**
* @internal
* The `atomic-product-numeric-field-value` component renders the value of a number product field.
*
* The number can be formatted by adding a `atomic-format-number`, `atomic-format-currency` or `atomic-format-unit` component into this component.
*/
@Component({
tag: 'atomic-product-numeric-field-value',
shadow: false,
})
export class AtomicProductNumber {
@InitializeBindings() public bindings!: Bindings;
@ProductContext() private product!: Product;

@Element() host!: HTMLElement;

@State() public error!: Error;

/**
* The field that the component should use.
* The component will try to find this field in the `Product.additionalFields` object unless it finds it in the `Product` object first.
*/
@Prop({reflect: true}) field!: string;

@State() formatter: NumberFormatter = defaultNumberFormatter;

@State() valueToDisplay: string | null = null;

@Listen('atomic/numberFormat')
public setFormat(event: CustomEvent<NumberFormatter>) {
event.preventDefault();
event.stopPropagation();
this.formatter = event.detail;
}

private parseValue() {
const value = ProductTemplatesHelpers.getProductProperty(
this.product,
this.field
);
if (value === null) {
return null;
}
const valueAsNumber = parseFloat(`${value}`);
if (Number.isNaN(valueAsNumber)) {
this.error = new Error(
`Could not parse "${value}" from field "${this.field}" as a number.`
);
return null;
}
return valueAsNumber;
}

private formatValue(value: number) {
try {
return this.formatter(value, this.bindings.i18n.languages as string[]);
} catch (error) {
this.error = error as Error;
return value.toString();
}
}

private updateValueToDisplay() {
const value = this.parseValue();
if (value !== null) {
this.valueToDisplay = this.formatValue(value);
}
}

componentWillRender() {
this.updateValueToDisplay();
}

public render() {
if (this.valueToDisplay === null) {
this.host.remove();
return;
}
return this.valueToDisplay;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import {Product} from '@coveo/headless/commerce';
import {Component, h, Prop} from '@stencil/core';
import {
InitializableComponent,
InitializeBindings,
} from '../../../../utils/initialization-utils';
import {CommerceBindings} from '../../atomic-commerce-interface/atomic-commerce-interface';
import {ProductContext} from '../product-template-decorators';

/**
* @internal
* The `atomic-product-price` component renders the price of a product.
*/
@Component({
tag: 'atomic-product-price',
shadow: false,
})
export class AtomicProductPrice
implements InitializableComponent<CommerceBindings>
{
@InitializeBindings() public bindings!: CommerceBindings;
public error!: Error;

@ProductContext() private product!: Product;

/**
* The currency to use in currency formatting. Allowed values are the [ISO 4217 currency codes](https://www.six-group.com/en/products-services/financial-information/data-standards.html#scrollTo=maintenance-agency), such as "USD" for the US dollar, "EUR" for the euro, or "CNY" for the Chinese RMB.
*/
@Prop({reflect: true}) public currency: string = 'USD';

public render() {
const hasPromotionalPrice =
this.product?.ec_promo_price !== undefined &&
this.product?.ec_price !== undefined &&
this.product?.ec_promo_price < this.product?.ec_price;

return (
<div>
<atomic-product-numeric-field-value
class={`mx-1 ${hasPromotionalPrice && 'text-error'}`}
field={hasPromotionalPrice ? 'ec_promo_price' : 'ec_price'}
>
<atomic-format-currency
currency={this.currency}
></atomic-format-currency>
</atomic-product-numeric-field-value>
{hasPromotionalPrice && (
<atomic-product-numeric-field-value
class="mx-1 text-xl line-through"
field="ec_price"
>
<atomic-format-currency
currency={this.currency}
></atomic-format-currency>
</atomic-product-numeric-field-value>
)}
</div>
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@
const commerceInterface = document.querySelector('atomic-commerce-interface');

await commerceInterface.initialize(commerceEngineConfig);

commerceInterface.executeFirstSearch();
})();
</script>
</head>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@
const commerceInterface = document.querySelector('atomic-commerce-interface');

await commerceInterface.initialize(commerceEngineConfig);

commerceInterface.executeFirstSearch();
})();
</script>
</head>
Expand All @@ -30,37 +28,6 @@ <h1>Product listing page</h1>
<atomic-commerce-query-summary></atomic-commerce-query-summary>
<atomic-commerce-sort-dropdown></atomic-commerce-sort-dropdown>
<atomic-commerce-product-list display="grid" density="compact" image-size="small">
<atomic-product-template>
<template>
<atomic-product-image
class="thumbnail"
field="ec_thumbnails"
fallback="https://picsum.photos/350"
></atomic-product-image>

<atomic-product-link class="font-bold"></atomic-product-link>

<atomic-field-condition class="brand text-neutral-dark" if-defined="ec_brand">
<atomic-product-text field="ec_brand"></atomic-product-text>
</atomic-field-condition>

<atomic-field-condition class="field" if-defined="ec_rating">
<atomic-result-rating field="ec_rating"></atomic-result-rating>
</atomic-field-condition>

<atomic-field-condition class="mt-4 text-2xl font-bold field" if-defined="ec_price">
<atomic-result-number field="ec_price">
<atomic-format-currency currency="CAD"></atomic-format-currency>
</atomic-result-number>
</atomic-field-condition>
<atomic-product-description></atomic-product-description>
<atomic-result-fields-list>
<atomic-field-condition must-match-ec_in_stock="true">
<atomic-result-badge label="In stock"></atomic-result-badge>
</atomic-field-condition>
</atomic-result-fields-list>
</template>
</atomic-product-template>
</atomic-commerce-product-list>
<atomic-commerce-load-more-products></atomic-commerce-load-more-products>
<atomic-commerce-pager></atomic-commerce-pager>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@
const commerceInterface = document.querySelector('atomic-commerce-interface');

await commerceInterface.initialize(commerceEngineConfig);

commerceInterface.executeFirstSearch();
})();
</script>
</head>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,8 @@ <h1>Search page</h1>
<atomic-result-rating field="ec_rating"></atomic-result-rating>
</atomic-field-condition>

<atomic-field-condition class="mt-4 text-2xl font-bold field" if-defined="ec_price">
<atomic-result-number field="ec_price">
<atomic-format-currency currency="CAD"></atomic-format-currency>
</atomic-result-number>
<atomic-field-condition class="mt-4 text-2xl field" if-defined="ec_price">
<atomic-product-price fallbackText="no price" currency="USD"></atomic-product-price>
</atomic-field-condition>
<atomic-product-description></atomic-product-description>
<atomic-result-fields-list>
Expand Down

0 comments on commit 67c9d80

Please sign in to comment.