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:
1
[key] : {
2
exclTax: number,
3
inclTax: number
4
hasDiscountCalculated: bool
5
}
Copied!

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.
1
/**
2
* Returns whether or not product is in stock
3
* @param product
4
* @param parentProduct
5
* @returns {boolean} (true if in stock | false if out of stock)
6
* @namespace Util/Product/Extract/getProductInStock
7
*/
8
export const getProductInStock = (product, parentProduct = {}) : bool
Copied!
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)
1
/**
2
* Returns price object for product.
3
* @param priceRange
4
* @param dynamicPrice
5
* @param adjustedPrice
6
* @param type
7
* @return object
8
* @namespace Util/Product/Extract/getPrice
9
*/
10
export const getPrice = (
11
priceRange,
12
dynamicPrice = false,
13
adjustedPrice = {},
14
type = PRODUCT_TYPE.simple
15
) :
16
{
17
{
18
// Contains original graphqlResponses (used to cehck ranges)
19
originalPrice: {
20
maxFinalPriceExclTax: {},
21
minFinalPrice: {},
22
minFinalPriceExclTax: {},
23
maxFinalPrice: {}
24
},
25
// Contains calculated prices
26
price: {
27
// Price without discount and tax
28
originalPriceExclTax: {
29
currency: string,
30
valueFormatted: string,
31
value: number
32
},
33
// Price without discount
34
originalPrice: {
35
currency: string,
36
valueFormatted: string,
37
value: number
38
},
39
// Price with discount and without tax
40
finalPriceExclTax: {
41
currency: string,
42
valueFormatted: string,
43
value: number
44
},
45
// Price with discount and tax
46
finalPrice: {
47
currency: string,
48
valueFormatted: string,
49
value: number
50
},
51
// Discount
52
discount: {
53
percentOff: number
54
}
55
}
56
}
57
}
Copied!
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.
1
/**
2
* Generates Magento type product interface for performing
3
* actions (add to cart, wishlist, exc.)
4
* @param product
5
* @param quantity
6
* @param parentProduct
7
* @param enteredOptions
8
* @param selectedOptions
9
* @returns {*[]}
10
* @namespace Util/Product/Transform/magentoProductTransform
11
*/
12
export const magentoProductTransform = (
13
product,
14
quantity = 1,
15
parentProduct = {},
16
enteredOptions = [],
17
selectedOptions = []
18
) : {[
19
sku: string!,
20
parent_sku: string,
21
quantity: number!,
22
selected_options: [string]!
23
entered_options: [{uid, value}]!
24
]}
Copied!
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) !!!
1
// Example of validation rule config
2
const rule = {
3
// Field is required
4
isRequired: true,
5
​
6
// Built-in validation, depends on type
7
inputType: INPUT_TYPES.text
8
​
9
// Regex | fn manual validation
10
match: /(())))/,
11
// If match is used as function you can return bool | string types
12
// if return type is not bool then outputed value will be used as error message
13
match: (value) => return value,
14
​
15
// Input range for: Numbers, Date, Time, etc.
16
// Length range for: Text, Area
17
range: {
18
min: 1,
19
max: 2
20
}
21
​
22
// Override of default error messages
23
customErrorMessages: {
24
onRequirementFail: __('Field is reqired'),
25
onInputyTypeFail: '',
26
onMatchFail: '',
27
onRangeFailMin: '',
28
onRangeFailMax: ''
29
}
30
};
31
​
32
// Example for Field group rule
33
const groupRule = {
34
// Field is required
35
isRequired: true,
36
​
37
selector: 'select, input'
38
​
39
// Fn manual validation
40
match: (values) => return values.some((value) => value),
41
42
// Override of error messags
43
customErrorMessages: {
44
onRequirementFail: 'Field is reqired',
45
onMatchFail: '',
46
onGroupFail: '' // Outputs error message if something in group failed, but component it self was ok
47
}
48
};
Copied!
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
1
export const VALIDATION_MESSAGES = {
2
//#region VALIDATION RULE MSG
3
isRequired: __('This field is required!'),
4
match: __('Incorrect input!'),
5
range: __('Value is out of range!'), // Range values are also in Validator.js as they require args
6
group: __('Field contains issues!'),
7
//#endregion
8
​
9
//#region VALIDATION RULE MSG
10
[VALIDATION_INPUT_TYPE.alpha]: __('Incorrect input! Alpha value required!'),
11
[VALIDATION_INPUT_TYPE.alphaNumeric]: __('Incorrect input! Alpha-Numeric value required!'),
12
[VALIDATION_INPUT_TYPE.alphaDash]: __('Incorrect input! Alpha-Dash value required!'),
13
[VALIDATION_INPUT_TYPE.url]: __('Incorrect input! URL required!'),
14
[VALIDATION_INPUT_TYPE.numeric]: __('Incorrect input! Numeric value required!'),
15
[VALIDATION_INPUT_TYPE.numericDash]: __('Incorrect input! Numeric-Dash value required!'),
16
[VALIDATION_INPUT_TYPE.integer]: __('Incorrect input! Integer required!'),
17
[VALIDATION_INPUT_TYPE.natural]: __('Incorrect input! Natural number required!'),
18
[VALIDATION_INPUT_TYPE.naturalNoZero]: __('Incorrect input!'),
19
[VALIDATION_INPUT_TYPE.email]: __('Incorrect email input!'),
20
[VALIDATION_INPUT_TYPE.date]: __('Incorrect date input!'),
21
[VALIDATION_INPUT_TYPE.phone]: __('Incorrect phone input!'),
22
[VALIDATION_INPUT_TYPE.password]: __('Incorrect password input!')
23
//#endregion
Copied!
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
1
/**
2
* Validates parameter based on rules
3
* @param value
4
* @param rule
5
* @returns {boolean|{errorMessages: *[], value}}
6
* @namespace Util/Validator/validate
7
*/
8
export const validate = (value, rule) :
9
// On pass:
10
true
11
// On fail:
12
{
13
errorMessages: []
14
value: (string|number)
15
}
Copied!
  • DOM validation, if passed rule then will check both children objects and itself if rule is not passed then checks only children.
1
/**
2
* Validates DOM object check itself and children
3
* @param DOM
4
* @param rule
5
* @returns {boolean|{errorMessages: *[], values: *[], errorFields: *[]}}
6
* @namespace Util/Validator/validateGroup
7
*/
8
export const validateGroup = (DOM, rule = null) :
9
// On pass:
10
true
11
// On fail:
12
{
13
values: [],
14
errorFields: [{
15
errorMessages: [],
16
value: (string|number)
17
}],
18
errorMessages: []
19
}
Copied!
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) :
1
// Example of how to define forms onSubmit()
2
onSubmit = (event, form, fields) => {}
3
​
4
// Example of how to define forms onError()
5
onError = (event, form, fields, errors) => {}
6
​
7
// Example of how to define forms|groups other events()
8
onBlur = (event, ...args, data: {...attr, form|group, fields}) => {}
9
​
10
// Example of how to define field events
11
onFocuss = (event, ...args, data: {...attr, type, field, value}) => {}
12
​
13
// Example of array output:
14
[
15
{
16
name: string,
17
type: string
18
value: string|number|bool
19
field: DOM object
20
}
21
]
22
​
23
// Example of object output:
24
{
25
name: {
26
name: string,
27
type: string
28
value: string|number|bool
29
field: DOM object
30
}
31
}
32
​
33
// To get name: value pair use transform function that work with both array and object output:
34
/**
35
* Returns name: value pair object for form output.
36
* @param fields (Array|Object)
37
* @returns {{}}
38
* @namespace Util/Form/Transform/transformToNameValuePair
39
*/
40
export const transformToNameValuePair = (fields) : { [name]: [value] }
Copied!
Field code example:
1
<Field
2
type={ FIELD_TYPE.checkbox }
3
label={ label }
4
attr={ {
5
id: `option-${ uid }`,
6
defaultValue: canChangeQuantity ? getEncodedBundleUid(uid, quantity) : uid,
7
name: `option-${ uid }`
8
} }
9
events={ {
10
onChange: updateSelectedValues
11
} }
12
validationRule={ {
13
match: this.getError.bind(this, quantity, stock, min, max)
14
} }
15
validateOn={ ['onChange'] }
16
/>
Copied!
As fields and forms are now stateless by default, to set default values requires to pass attributes defaultValue or defaultChecked
Field group example:
1
<FieldGroup
2
validationRule={ {
3
isRequired,
4
selector: '[type="checkbox"]'
5
} }
6
validateOn={ ['onChange'] }
7
>
8
{ options.map(this.renderCheckBox) }
9
</FieldGroup>
Copied!
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:
1
get fieldMap() {
2
return [
3
// Field
4
{
5
attr: {
6
name: __('Email'),
7
...
8
},
9
events: {
10
onChange: handleInput,
11
...
12
},
13
validateOn: [ 'onChange', ... ],
14
validationRules: {
15
isRequired: true,
16
...
17
},
18
...
19
},
20
// FieldGroup
21
{
22
attr: { ... }
23
events: { ... }
24
fields: [
25
// Can contain both fields or field groups
26
],
27
...
28
},
29
...
30
];
31
}
Copied!
  • 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
1
const {
2
maximum_price: {
3
final_price: maxFinalPrice = {},
4
final_price_excl_tax: maxFinalPriceExclTax = {}
5
} = {}
6
} = priceRange || {};
Copied!
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. 1.
    Global - all components that call function will store value-response pairs in shared space
  2. 2.
    Local - component will cache its function value-response pair in separated space. ( In development... currently not priority )
1
/**
2
* Returns result from global cache for fn
3
* @param {function} fn
4
* @param {array} args
5
* @returns function response
6
* @namespace Util/Cache/fromCache
7
*/
8
export const fromCache = (fn, args): fn(...args)
Copied!
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:
1
JSON.stringify({
2
file_name: String
3
file_data: Base64 value
4
});
Copied!
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:
1
// FROM
2
const FIELD_TEXT = ...
3
const FIELD_FILE = ...
4
...
5
​
6
// TO
7
const FIELD_TYPE = {
8
text: ...,
9
file: ...,
10
....
11
};
Copied!
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