Release notes

5.1.0: Code Refactoring

Bundle Products, Customizable Options, Forms, Stock, and Price calculations

1. Unified ProductCard and ProductAction

There is a new component and container that unifies both product base structure and base functionality - ProductContainer & Product

It is no longer required to implement the same functionally in different ways between ProductCard and ProductAction, now they both extend from Product.

Shared functionality:

  • Option configuration (custom, config, bundle, links, grouped)

  • Adding to cart

  • Adding to compare list

  • Adding to wishlist

  • Quantity changer

  • Price output

  • Stock output

  • Title, Brand, rating, samples .... renderers

Architectural changes:

  • No longer used variantIndex, replaced with product to get base product or getActiveProduct to get variant or base product. (returns base product if variant not set)

  • Integrated global: stock status, price and qty range.

  • Quantity now can be number or array - for grouped products it is array and for all other products it is number.

  • Customizable and bundle options are stored in enteredOptions and selectedOptions

  • All added prices are stored in object adjustedPrices (prices for bundle, custom, downloadable links, exc.), they must follow format:

[key] : {
	exclTax: number,
	inclTax: number
	hasDiscountCalculated: bool
}

2. New Custom, Bundle components

Complete redesign of customizable and bundle options from scratch.

Features supported:

  • All BE field types are now supported ( +radio, +date, +date-time, +time )

  • Price updates

  • Validation and error messages for BE configs

  • Validation for product stock and quantity range

  • Use of UID (uid is base64 encoded value that includes key/option id/quantity)

  • Stores values in entered_options and selected_options

  • Fields are validated on event onChange thus giving real time response

  • Price is updated on event onChange thus giving real time response

  • Quantity changer support for all field types in bundle options

Note: "Min quantity 3!" error message indicates that product - "test-simple", requires minimum quantity of 3 to be purchased. "Field contains issues!" error message indicates that FieldGroup contains error (more info in validation and form section).

3. Globalized stock, price & Magento product.

3.1 Stock

New function that calculates stock status based on passed product, no longer required different stock checks between function, now all stock status calculations are done within this function.

Function checks stock for products based on their type.

/**
 * Returns whether or not product is in stock 
 * @param product
 * @param parentProduct
 * @returns {boolean} (true if in stock | false if out of stock)
 * @namespace Util/Product/Extract/getProductInStock
 */
export const getProductInStock = (product, parentProduct = {}) : bool

3.2. Price

New function that generates price object that should be used within project. Prices are calculated based on product type and their adjusted prices (prices that are added from links, customization, bundles, exc.).

(currently no support for tier price, meaning doesn't use tier price rule to update value based on quantity, but this feature will be added later, as its quite simple to add)

/**
 * Returns price object for product.
 * @param priceRange
 * @param dynamicPrice
 * @param adjustedPrice
 * @param type
 * @return object
 * @namespace Util/Product/Extract/getPrice
 */
export const getPrice = (
    priceRange,
    dynamicPrice = false,
    adjustedPrice = {},
    type = PRODUCT_TYPE.simple
) : 
{
    {
        // Contains original graphqlResponses (used to cehck ranges)
        originalPrice: {
            maxFinalPriceExclTax: {}, 
            minFinalPrice: {}, 
            minFinalPriceExclTax: {}, 
            maxFinalPrice: {}
        }, 
        // Contains calculated prices
        price: {
            // Price without discount and tax
            originalPriceExclTax: {
                currency: string, 
                valueFormatted: string,
                value: number
            }, 
            // Price without discount
            originalPrice: {
                currency: string, 
                valueFormatted: string,
                value: number
            }, 
            // Price with discount and without tax
            finalPriceExclTax: {
                currency: string, 
                valueFormatted: string,
                value: number
            }, 
            // Price with discount and tax
            finalPrice: {
                currency: string, 
                valueFormatted: string,
                value: number 
            }, 
            // Discount
            discount: {
                percentOff: number
            }
        }
    }
}

3.2.1. Updated ProductPrice component

ProductPrice now uses price gotten from getPrice() function.

ProductPrice contains preview mode by setting property isPreview to true. Preview mode is the one that is visible usually on PLP, that instead of showing price, outputs "price ranges". Preview labels are now baked into ProductPrice instead of being randomly passed form different components.

Supported preview prices:

  • Product with tax

  • Product with discount

  • Product with tier price

  • Product with customizable price

  • Bundle product

  • Downloadable product

  • Configurable product

  • Out of stock

  • Fixed max range excl tax price (issue can be seen in image bellow for product Dynamic Bundle)

3.3. Magento Product

Function that generates Magento compatible product for graphql use.

/**
 * Generates Magento type product interface for performing
 * actions (add to cart, wishlist, exc.)
 * @param product
 * @param quantity
 * @param parentProduct
 * @param enteredOptions
 * @param selectedOptions
 * @returns {*[]}
 * @namespace Util/Product/Transform/magentoProductTransform
 */
export const magentoProductTransform = (
    product,
    quantity = 1,
    parentProduct = {},
    enteredOptions = [],
    selectedOptions = []
) : {[
	sku: string!,
	parent_sku: string,
	quantity: number!,
  selected_options: [string]!
	entered_options: [{uid, value}]!
]}

3.4. Other functions

There are many other useful functions that are stored in Util/Product/Transform and Util/Product/Extract

4. New Validation utility and New Forms

4.1. Validation utility

New utility - Validation, that can be used for both property checks and DOM object checks, thus removing previously limited validation that was directly added only in fields and forms.

4.1.1. Validation rule structure:

!!! All rule fields are optional (AKA not required) !!!

// Example of validation rule config
const rule = {
  // Field is required
  isRequired: true,

  // Built-in validation, depends on type
  inputType: INPUT_TYPES.text

  // Regex | fn manual validation
  match: /(())))/,
  // If match is used as function you can return bool | string types
  // if return type is not bool then outputed value will be used as error message
  match: (value) => return value,

  // Input range for: Numbers, Date, Time, etc.
  // Length range for: Text, Area
  range: {
    min: 1,
    max: 2
  }

  // Override of default error messages
  customErrorMessages: {
    onRequirementFail: __('Field is reqired'),
    onInputyTypeFail: '',
    onMatchFail: '',
    onRangeFailMin: '',
    onRangeFailMax: ''
  }
};

// Example for Field group rule
const groupRule = {
  // Field is required
  isRequired: true,

	selector: 'select, input'

  // Fn manual validation
  match: (values) => return values.some((value) => value),
  
  // Override of error messags
  customErrorMessages: {
    onRequirementFail: 'Field is reqired',
    onMatchFail: '',
		onGroupFail: '' // Outputs error message if something in group failed, but component it self was ok
  }
};

4.1.2. Globalized error messages:

If there is no custom error message provided, then default error message for specific validation type will be outputted.

Util/Validator/Config.js

export const VALIDATION_MESSAGES = {
    //#region VALIDATION RULE MSG
    isRequired: __('This field is required!'),
    match: __('Incorrect input!'),
    range: __('Value is out of range!'), // Range values are also in Validator.js as they require args
    group: __('Field contains issues!'),
    //#endregion

    //#region VALIDATION RULE MSG
    [VALIDATION_INPUT_TYPE.alpha]: __('Incorrect input! Alpha value required!'),
    [VALIDATION_INPUT_TYPE.alphaNumeric]: __('Incorrect input! Alpha-Numeric value required!'),
    [VALIDATION_INPUT_TYPE.alphaDash]: __('Incorrect input! Alpha-Dash value required!'),
    [VALIDATION_INPUT_TYPE.url]: __('Incorrect input! URL required!'),
    [VALIDATION_INPUT_TYPE.numeric]: __('Incorrect input! Numeric value required!'),
    [VALIDATION_INPUT_TYPE.numericDash]: __('Incorrect input! Numeric-Dash value required!'),
    [VALIDATION_INPUT_TYPE.integer]: __('Incorrect input! Integer required!'),
    [VALIDATION_INPUT_TYPE.natural]: __('Incorrect input! Natural number required!'),
    [VALIDATION_INPUT_TYPE.naturalNoZero]: __('Incorrect input!'),
    [VALIDATION_INPUT_TYPE.email]: __('Incorrect email input!'),
    [VALIDATION_INPUT_TYPE.date]: __('Incorrect date input!'),
    [VALIDATION_INPUT_TYPE.phone]: __('Incorrect phone input!'),
    [VALIDATION_INPUT_TYPE.password]: __('Incorrect password input!')
    //#endregion

Note: In the newest version, input type validation is not performed if the value is empty, so it will output in this case only the message "This field is required!", but image still works as example to show multiple error msg output.

4.1.3. Validation functions:

There are two validation functions:

  • Value validation

/**
 * Validates parameter based on rules
 * @param value
 * @param rule
 * @returns {boolean|{errorMessages: *[], value}}
 * @namespace Util/Validator/validate
 */
export const validate = (value, rule) :
// On pass:
true
// On fail:
{
	errorMessages: []
	value: (string|number)
}
  • DOM validation, if passed rule then will check both children objects and itself if rule is not passed then checks only children.

/**
 * Validates DOM object check itself and children
 * @param DOM
 * @param rule
 * @returns {boolean|{errorMessages: *[], values: *[], errorFields: *[]}}
 * @namespace Util/Validator/validateGroup
 */
export const validateGroup = (DOM, rule = null) :
// On pass:
true
// On fail:
{
	values: [],
	errorFields: [{
		errorMessages: [],
		value: (string|number)	
	}],
	errorMessages: []
}

4.2. Form, Fields, FieldGroup, FieldForm

Complete redesign of Forms, Fields, FieldForm.

New features:

  • Forms code now is simpler, smaller, faster and more extendable.

  • Fields and Field groups contain now validation event

  • New component Field Group

  • Integrated support for new Validation Utility

  • Stateless by default (reduces render count on update)

  • Support of all field types

  • For field type file - outputs plural type messages if contains attr multi, otherwise uses singular form.

  • Fields can output "*" next to label if parameter addRequiredTag is set to true.

  • Form onSubmit passes form and fields to passed functions onSubmit and onError ether in object mode or in array mode (depends on parameter returnAsObject) :

// Example of how to define forms onSubmit()
onSubmit = (event, form, fields) => {}

// Example of how to define forms onError()
onError = (event, form, fields, errors) => {}

// Example of how to define forms|groups other events()
onBlur = (event, ...args, data: {...attr, form|group, fields}) => {}

// Example of how to define field events
onFocuss = (event, ...args, data: {...attr, type, field, value}) => {}

// Example of array output:
[
	{
		name: string,
		type: string
		value: string|number|bool
		field: DOM object
	}
]

// Example of object output:
{
	name: {
		name: string,
		type: string
		value: string|number|bool
		field: DOM object
	}
}

// To get name: value pair use transform function that work with both array and object output:
/**
 * Returns name: value pair object for form output.
 * @param fields (Array|Object)
 * @returns {{}}
 * @namespace Util/Form/Transform/transformToNameValuePair
 */
export const transformToNameValuePair = (fields) : { [name]: [value] }

Field code example:

<Field
  type={ FIELD_TYPE.checkbox }
  label={ label }
  attr={ {
      id: `option-${ uid }`,
      defaultValue: canChangeQuantity ? getEncodedBundleUid(uid, quantity) : uid,
      name: `option-${ uid }`
  } }
  events={ {
      onChange: updateSelectedValues
  } }
  validationRule={ {
      match: this.getError.bind(this, quantity, stock, min, max)
  } }
  validateOn={ ['onChange'] }
/>

As fields and forms are now stateless by default, to set default values requires to pass attributes defaultValue or defaultChecked

Field group example:

<FieldGroup
  validationRule={ {
      isRequired,
      selector: '[type="checkbox"]'
  } }
  validateOn={ ['onChange'] }
>
    { options.map(this.renderCheckBox) }
</FieldGroup>

Field Groups are containers for fields, their use case is quite simple:

  • Field group passes validation only if all its fields are valid and if rules applied to it self are valid

  • Field group validation rule isRequired is used to ensure that at least one field contains value. (Example: you have 4 checkboxes, to submit form you need at-least one of them to be selected, to do that you would wrap those checkboxes in FieldGroup add add isRequired rule in validations, similarly if required at-least two checkboxes you can wrap them in FieldGroup and add match rule in validation)

All components are updated to use new forms and fields

4.3. FieldForm

Architectural changes are made for FieldForm component, it now utilizes new syntax for fields and supports multilayered sectioning via FieldGroup component:

Example of new fieldMap:

get fieldMap() {
        return [
            // Field
            {
                attr: {
                    name: __('Email'),
                    ...
                },
                events: {
                    onChange: handleInput,
                    ...
                },
                validateOn: [ 'onChange', ... ],
                validationRules: {
                    isRequired: true,
                    ...
                },
                ...
            },
            // FieldGroup
            {
                attr: { ... }
                events: { ... }
                fields: [
                    // Can contain both fields or field groups
                ],
								...
            },
						...
        ];
    }
  • Now field map returns array instead of object.

  • If object contains parameter fields then it will render FieldGroup.

  • Notice that fields can contain both objects for fields and for field groups, thus allowing to create complex structures with undefined depth.

  • Form parameters are assigned from function getFormProps, by default it will return ...this.props.

Note: FieldForm form structures are moved out to files called [form-name].form.js, to reduce size of main component, and make forms more readable + we can utilize function caching for that.

Note for me (not in this PR): Add possibility off accessing values in same group/form when doing validation & attribute setting in fields themself AKA make fields in same group aware of other fields. Example: when setting isDisabled, I want option to access different field values without storing them in separate parameter, as it was done before.... so it would look something like this isDisabled: ({ [differentFieldName]: { value } }) β‡’ value && value.length > 10

5. Smaller changes

5.1. Updated downloadable, grouped components

Downloadable and Grouped options are updated to use new Product.container and new Form

5.1.1. Grouped product

  • If grouped product contains product with tier prices it will also output them (same as in Magento):

  • Images no longer show 'Image no specified' (check image above - kept as example):

  • Images use correct thumbnail source, instead of wrong one that outputted large images. (I guess small images where previously loaded)

  • FE validation for quantity and stock:

5.2. Commenting

All new code contains functional, class and attribute comments.

New code also includes region comments //#region , to increase readability in IDE (in imagge bellow GETTERS & EVENTS are collapsed).

5.3. Naming

All new code follows naming schema that on attribute destruction converts snake_case to camelCase

Example

const {
    maximum_price: {
        final_price: maxFinalPrice = {},
        final_price_excl_tax: maxFinalPriceExclTax = {}
    } = {}
} = priceRange || {};

5.4. Function caching

Added utility to cache functions based on their input, to improve performance on functions with larger complexity.

Functions can be cached in two modes:

  1. Global - all components that call function will store value-response pairs in shared space

  2. Local - component will cache its function value-response pair in separated space. ( In development... currently not priority )

/**
 * Returns result from global cache for fn
 * @param {function} fn
 * @param {array} args
 * @returns function response
 * @namespace Util/Cache/fromCache
 */
export const fromCache = (fn, args): fn(...args)

Note: When using this function args should be as specific as possible, because key is generated from JSON.stringify(args) (example: instead of passing whole product object, pass fields that are required, for example price_range)

5.5. Deprecated saveCartItem mutation

No longer used mutation saveCartItem instead now we are using Magento native graphql mutation addProductsToCart and updateCartItems

Currently only saveCartItem is migrated to native graphql mutation, thus there are two request made while saving cart item - first one for adding products, second one to get updated cart. This will be changed in graphql update commit.

scandipwa/quote-graphql also contains patch for addProductsToCart to solve Magento issue for multiple checkbox selection for bundle bundles. (Example: Product2 options)

also added file upload support by passing (code bellow) into value field:

JSON.stringify({
	file_name: String
	file_data: Base64 value
});

5.6. Deprecated saveWishlistItem mutation

No longer used mutation saveWishlistItem instead now we are using Magento native graphql mutation addProductsToWishlist

Currently only saveWishlistItem is migrated to native graphql mutation, thus there are two request made while adding item - first one for adding products, second one to get updated wishlist. This will be changed in graphql update commit.

5.7. Configs

Few configurations are combined into object based configuration.

Example:

// FROM
const FIELD_TEXT = ...
const FIELD_FILE = ...
...

// TO
const FIELD_TYPE = {
	text: ...,
	file: ...,
  ....
};

Reason:

  • Reduces import count

  • Simpler to read and remember

  • IDE will output all options when accessing object

  • You can use PropTypes.oneOf(Object.values(CONFIG)) in props and in other places.

5.8. Other

  • Cart item options outputted based on their type

  • Quantity changer is disabled for out of stock product:

  • In PLP if product out of stock Add to cart button is in disabled state, if product in stock and requires configuration (customizable, config, bundle, download links) outputs Configure button if product in stock and doesn't require any changes, then outputs add to cart button.

  • If parent is set as Out of stock, then active variant will also show out of stock (same as Magento) + options will be shown as disabledof

Last updated