Release notes
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 orgetActiveProduct
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
andselectedOptions
- 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
}
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.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.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 functionsonSubmit
andonError
ether in object mode or in array mode (depends on parameterreturnAsObject
) :
// 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.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))
inprops
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 modified 1yr ago