commercetools-8.x-1.2-alpha1/modules/commercetools_decoupled/js/organisms/CheckoutForm.js
modules/commercetools_decoupled/js/organisms/CheckoutForm.js
class CheckoutForm extends HTMLElement {
async connectedCallback() {
this.cart = {};
this.isLoading = true;
this.customerAddressData = null;
this.predefinedAddresses = [];
this.isAnonymous = CheckoutForm.isAnonymous();
this.renderForm();
this.cart = await window.commercetools.getCart();
if (this.cart?.shippingAddress?.id === this.cart?.billingAddress?.id) {
delete this.cart.billingAddress;
}
this.customerAddressData = this.isAnonymous
? null
: await window.commercetools.getUserAddressData();
this.predefinedAddresses.shippingAddress = this.predefineAddress(
this.cart,
this.customerAddressData,
'shippingAddress',
);
this.predefinedAddresses.billingAddress = this.predefineAddress(
this.cart,
this.customerAddressData,
'billingAddress',
);
this.isLoading = false;
this.renderForm();
}
predefineAddress(cart, customerAddressData, addressType) {
if (cart[addressType] != null) {
return cart[addressType];
}
if (this.isAnonymous) {
return null;
}
const defaultIdKey = CheckoutForm.DEFAULT_ADDRESS_KEY_MAP?.[addressType];
const defaultAddressId = customerAddressData[defaultIdKey];
const listKey = CheckoutForm.ADDRESS_KEY_MULTIPLE[addressType];
const list = customerAddressData[listKey] || [];
return list.find((addr) => addr.id === defaultAddressId) || null;
}
renderForm() {
this.innerHTML = `
<form data-addresses='${this.prepareAddressForAttr()}' class="commercetools-content-order-submission ${this.isLoading ? 'placeholderify' : ''}" data-drupal-selector="commercetools-order-submission" action="/checkout" method="post" id="commercetools-order-submission" accept-charset="UTF-8">
<fieldset class="shadow p-3 bg-primary bg-opacity-10 form-item form-wrapper shipping-address">
<div class="fieldset-wrapper">
<h2 class="pb-3 mb-4 border-bottom">${Drupal.t('Shipping address')}</h2>
<div class="shippingAddress-fields"></div>
</div>
</fieldset>
<div class="mt-4 mb-4 shadow p-3 bg-primary bg-opacity-10">
<div class="form-item form-type-checkbox form-item-sameAddress">
<input type="checkbox" id="edit-sameAddress" name="sameAddress" value="1" ${this.cart?.billingAddress ? '' : `checked="checked"`} class="form-checkbox form-check-input">
<label for="edit-sameAddress">${Drupal.t('Billing address same as shipping address')}</label>
</div>
</div>
<fieldset class="shadow p-3 bg-primary bg-opacity-10 form-item form-wrapper billing-address">
<div class="fieldset-wrapper">
<h2 class="pb-3 mb-4 border-bottom">${Drupal.t('Billing address')}</h2>
<div class="billingAddress-fields"></div>
</div>
</fieldset>
<div data-drupal-selector="edit-actions" class="form-actions js-form-wrapper form-wrapper" id="edit-actions">
<input type="submit" id="edit-submit" name="op" value="${Drupal.t('Save & Continue')}" class="button form-submit btn btn-primary">
</div>
</form>
`;
['shippingAddress', 'billingAddress'].forEach((addressType) => {
const fields = CheckoutForm.addressFields();
const fieldsWr = this.querySelector(`.${addressType}-fields`);
fields.forEach((fieldProperties) => {
const fieldHTML = this.createElement(addressType, fieldProperties);
fieldsWr.append(fieldHTML);
});
if (!this.isAnonymous) {
this.addAdditionalFields(fieldsWr, addressType);
// this.addUserAddresses(fieldsWr, addressType);
}
});
// Display/hide billing address section according to the "Same address" checkbox.
const billingAddress = this.querySelector('.billing-address');
const sameCheckbox = this.querySelector('input[name="sameAddress"]');
CheckoutForm.displayInputs(billingAddress, sameCheckbox?.checked || false);
this.querySelector('[name="sameAddress"]').addEventListener('change', (e) =>
CheckoutForm.displayInputs(billingAddress, e.target.checked),
);
this.querySelector('form').addEventListener(
'submit',
CheckoutForm.submitFormEvent,
);
}
addAdditionalFields(fieldsWr, addressType) {
const additionalFields = CheckoutForm.additionalFields();
const additionalFieldsElements = {};
additionalFields.forEach((fieldProperties) => {
additionalFieldsElements[fieldProperties.name] = this.createElement(
addressType,
fieldProperties,
);
fieldsWr.append(additionalFieldsElements[fieldProperties.name]);
});
CheckoutForm.displayInputs(additionalFieldsElements.title, true);
CheckoutForm.displayInputs(additionalFieldsElements.setDefault, true);
additionalFieldsElements.save.addEventListener('change', (e) => {
CheckoutForm.displayInputs(
additionalFieldsElements.title,
!e.target.checked,
);
CheckoutForm.displayInputs(
additionalFieldsElements.setDefault,
!e.target.checked,
);
});
const createOptionElement = (props = {}) => {
const optionElement = document.createElement('option');
Object.assign(optionElement, props);
return optionElement;
};
const addressKeyMultiple = CheckoutForm.ADDRESS_KEY_MULTIPLE[addressType];
const addresses = this.customerAddressData?.[addressKeyMultiple];
if (addresses != null) {
const defaultAddressId =
this.predefinedAddresses?.[addressType]?.id || 'new';
const select = document.createElement('select');
select.textContent = Drupal.t('Fill a new address or choose from saved');
select.dataset.addressType = addressType;
select.dataset.previousValue = defaultAddressId;
select.classList.add('form-select');
select.name = `${addressType}.id`;
select.id = `${addressType}.id`;
const selectLabel = document.createElement('label');
selectLabel.htmlFor = select.id;
selectLabel.textContent = Drupal.t(
'Fill a new address or choose from saved',
);
const selectContainer = document.createElement('div');
selectContainer.append(selectLabel, select);
const newOption = createOptionElement({
value: 'new',
textContent: Drupal.t('Add a new address'),
});
select.appendChild(newOption);
this.customerAddressData?.[addressKeyMultiple].forEach((address) => {
const option = createOptionElement({
value: address.id,
textContent: address.title || address.id,
selected: address.id === defaultAddressId,
});
select.appendChild(option);
});
select.addEventListener('change', (e) => {
CheckoutForm.disableInputs(fieldsWr, e.target.value !== 'new');
CheckoutForm.displayInputs(
additionalFieldsElements.save,
e.target.value !== 'new',
);
CheckoutForm.selectAction(
e.target.dataset.addressType,
e.target.value,
e.target.dataset.previousValue,
);
e.target.dataset.previousValue = e.target.value;
});
fieldsWr.parentNode.insertBefore(selectContainer, fieldsWr);
CheckoutForm.disableInputs(fieldsWr, defaultAddressId !== 'new');
CheckoutForm.displayInputs(
additionalFieldsElements.save,
defaultAddressId !== 'new',
);
}
}
createElement(addressType, fieldProperties) {
fieldProperties.defaultValue =
this.predefinedAddresses?.[addressType]?.[fieldProperties.name] ||
fieldProperties.defaultValue ||
'';
const fieldHTML = document.createElement(
`ct-input-${fieldProperties.type}`,
);
fieldHTML.properties = {
...fieldProperties,
name: `${addressType}.${fieldProperties.name}`,
};
return fieldHTML;
}
static displayInputs(element, hide) {
if (hide === true) {
element.classList.add('d-none');
element
.querySelectorAll('.required')
.forEach((el) => el.removeAttribute('required'));
} else {
element.classList.remove('d-none');
element
.querySelectorAll('.required')
.forEach((el) => el.setAttribute('required', 'required'));
}
}
static disableInputs(element, disable) {
if (disable === true) {
element
.querySelectorAll('.form-item input')
.forEach((el) => el.setAttribute('disabled', 'disabled'));
element
.querySelectorAll('.required')
.forEach((el) => el.removeAttribute('required'));
} else {
element
.querySelectorAll('.form-item input:not(.disabled)')
.forEach((el) => el.removeAttribute('disabled'));
element
.querySelectorAll('.required')
.forEach((el) => el.setAttribute('required', 'required'));
}
}
static selectAction(
addressType,
selectedAddressId,
previousSelectedAddressId,
) {
const addressTypeMultiple = CheckoutForm.ADDRESS_KEY_MULTIPLE[addressType];
const form = document.getElementById('commercetools-order-submission');
const addresses = CheckoutForm.getAddresses();
const selectedAddress = addresses[addressTypeMultiple][selectedAddressId];
if (previousSelectedAddressId === 'new') {
form
.querySelectorAll(`.${addressType}-fields input`)
.forEach((element) => {
const parts = element.name.split('.');
const fieldName = parts[1] || element.name;
addresses[addressTypeMultiple].new[fieldName] =
element.type === 'checkbox' ? element.checked : element.value;
});
this.storeAddresses(addresses);
}
CheckoutForm.disableInputs(
form.querySelector(`.${addressType}-fields`),
selectedAddressId !== 'new',
);
CheckoutForm.setAddressValues(form, addressType, selectedAddress);
}
static setAddressValues(form, addressType, selectedAddressData) {
Object.keys(selectedAddressData).forEach((fieldName) => {
const input = form.querySelector(
`.${addressType}-fields [name="${addressType}.${fieldName}"]`,
);
if (!input) return;
if (input.type === 'checkbox' || input.type === 'radio') {
input.checked = Boolean(selectedAddressData[fieldName]);
} else {
input.value = selectedAddressData[fieldName] ?? '';
}
});
}
static async submitFormEvent(e) {
e.preventDefault();
e.target.classList.add('placeholderify');
// FormData doesn’t include disabled fields.
e.target
.querySelectorAll('input[disabled="disabled"]')
.forEach((el) => el.removeAttribute('disabled'));
// Get form values.
const formData = new FormData(e.target);
const values = {};
formData.forEach((v, k) => {
if (k.includes('.')) {
const [prefix, fieldName] = k.split('.');
if (typeof values[prefix] === 'undefined') {
values[prefix] = {};
}
values[prefix][fieldName] = v;
} else {
values[k] = v;
}
});
const { sameAddress } = values;
let { shippingAddress, billingAddress } = values;
if (!CheckoutForm.isAnonymous()) {
const saveAddress = async function (address, type) {
return window.commercetools.addAddressCustomer({
addressData: CheckoutForm.filterAddressValues(address),
type,
setDefault: address.setDefault === 'on',
});
};
if (shippingAddress.id === 'new' && shippingAddress.save === 'on') {
shippingAddress = await saveAddress(shippingAddress, 'shippingAddress');
}
if (
!sameAddress &&
billingAddress.id === 'new' &&
billingAddress.save === 'on'
) {
billingAddress = await saveAddress(billingAddress, 'billingAddress');
}
}
// Add shipping and billing addresses necessary for creating order.
const actions = [
{
setShippingAddress: {
address: CheckoutForm.filterAddressValues(shippingAddress),
},
},
{
setBillingAddress: {
address: sameAddress
? CheckoutForm.filterAddressValues(shippingAddress)
: CheckoutForm.filterAddressValues(billingAddress),
},
},
];
const cart = await window.commercetools.updateCart({
id: '[current_cart_or_create:id]',
version: '[current_cart_or_create:version]',
actions,
});
// Create order by using cart.
const order = await window.commercetools.createOrderFromCart({
id: cart.id || '[current_cart:id]',
version: cart.version || '[current_cart:version]',
});
// Remove cart cookie and redirect to the order page.
document.cookie = `${drupalSettings.commercetoolsDecoupled.sessionCookieName}=; Max-Age=-1;`;
const orderPath = `${drupalSettings.commercetoolsDecoupled.orderPathPrefix}/${order.id}`;
window.location.href = orderPath;
}
static isAnonymous() {
return drupalSettings.user.uid === 0;
}
static addressFields() {
return [
{
label: Drupal.t('First name'),
type: 'textfield',
name: 'firstName',
required: true,
disabled: false,
},
{
label: Drupal.t('Last name'),
type: 'textfield',
name: 'lastName',
required: true,
disabled: false,
},
{
label: Drupal.t('Email'),
type: 'email',
name: 'email',
required: true,
},
{
label: Drupal.t('Country'),
type: 'textfield',
name: 'country',
required: true,
disabled: true,
defaultValue: window.drupalSettings.commercetoolsDecoupled.priceCountry,
},
{
label: Drupal.t('Street Address'),
type: 'textfield',
name: 'streetName',
required: true,
disabled: false,
},
{
label: Drupal.t('Street number'),
type: 'textfield',
name: 'streetNumber',
required: false,
disabled: false,
},
{
label: Drupal.t('State'),
type: 'textfield',
name: 'state',
required: true,
disabled: false,
},
{
label: Drupal.t('City'),
type: 'textfield',
name: 'city',
required: true,
disabled: false,
},
{
label: Drupal.t('ZIP code'),
type: 'textfield',
name: 'postalCode',
required: true,
disabled: false,
},
{
label: Drupal.t('Phone number'),
type: 'textfield',
name: 'phone',
required: true,
disabled: false,
},
];
}
static additionalFields() {
return [
{
label: Drupal.t('Save this new address to reuse in the next orders'),
type: 'checkbox',
name: 'save',
checked: false,
required: false,
disabled: false,
},
{
label: Drupal.t(
'Fill the title to name this address in the list of saved addresses',
),
type: 'textfield',
name: 'title',
required: false,
disabled: false,
},
{
label: Drupal.t('Use this address as default for the next orders'),
type: 'checkbox',
name: 'setDefault',
checked: false,
required: false,
disabled: false,
},
];
}
static filterAddressValues(addressData) {
const addressesFields = window.commercetools.addAddressNodeFields();
if (addressData.id === 'new') {
delete addressData.id;
}
return Object.fromEntries(
Object.entries(addressData).filter(([key]) => addressesFields[key]),
);
}
prepareAddressForAttr() {
const addressData = this.customerAddressData;
if (addressData === null) {
return '';
}
const data = {
shippingAddresses: Object.fromEntries(
addressData.shippingAddresses.map((addr) => [addr.id, addr]),
),
billingAddresses: Object.fromEntries(
addressData.billingAddresses.map((addr) => [addr.id, addr]),
),
};
const emptyNewAddress = Object.fromEntries(
Object.keys(window.commercetools.addAddressNodeFields()).map((key) => [
key,
'',
]),
);
emptyNewAddress.country =
window.drupalSettings.commercetoolsDecoupled.priceCountry;
data.shippingAddresses.new = emptyNewAddress;
data.billingAddresses.new = emptyNewAddress;
return JSON.stringify(data);
}
static getAddress(addressType, id) {
return CheckoutForm.getAddresses()?.[addressType]?.[id];
}
static getAddresses() {
const form = document.getElementById('commercetools-order-submission');
const jsonAddressesData = form.getAttribute('data-addresses');
return jsonAddressesData ? JSON.parse(jsonAddressesData) : null;
}
static storeAddresses(addresses) {
const form = document.getElementById('commercetools-order-submission');
form.setAttribute('data-addresses', JSON.stringify(addresses));
}
}
CheckoutForm.ADDRESS_KEY_MULTIPLE = {
shippingAddress: 'shippingAddresses',
billingAddress: 'billingAddresses',
};
CheckoutForm.DEFAULT_ADDRESS_KEY_MAP = {
shippingAddress: 'defaultShippingAddressId',
billingAddress: 'defaultBillingAddressId',
};
customElements.define('ct-checkout-form', CheckoutForm);
