Skip to content

Commit

Permalink
feat: basic support for dynamic forms
Browse files Browse the repository at this point in the history
Loading from XML definition files.
  • Loading branch information
vysinsky committed Jun 20, 2022
1 parent 9cc2774 commit ad031ff
Show file tree
Hide file tree
Showing 8 changed files with 195 additions and 151 deletions.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"devDependencies": {
"@commitlint/cli": "^16.2.4",
"@commitlint/config-conventional": "^16.2.4",
"@types/xml2js": "^0.4.11",
"concurrently": "^7.2.0",
"husky": "^8.0.1",
"jest": "^28.1.0",
Expand All @@ -58,7 +59,8 @@
"deepmerge": "^4.2.2",
"dw-api-mock": "git+https://github.com/SalesforceCommerceCloud/dw-api-mock.git",
"express": "^4.18.1",
"module-alias": "^2.2.2"
"module-alias": "^2.2.2",
"xml2js": "^0.4.23"
},
"lint-staged": {
"*.{js,ts,jsx,tsx,json,md}": "yarn format"
Expand Down
93 changes: 93 additions & 0 deletions server/src/forms/FormsLoader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
const { locateFormDefinitionFile } = require('../utils');
const { readFileSync } = require('cosmiconfig/dist/readFile');
const xml2js = require('xml2js');
const FormGroup = require('dw/web/FormGroup');
const FormField = require('dw/web/FormField');

class FormsLoader {
constructor() {
const self = this;
this._accessor = new Proxy(
{},
{
get(obj, formName) {
return self.getForm(formName);
},
}
);
}

getAccessor() {
return this._accessor;
}

getForm(formName) {
const { cartridge, path } = locateFormDefinitionFile(
formName,
request.locale
);

let formDefinition;
xml2js.parseString(readFileSync(path), (error, result) => {
if (error) {
throw error;
}

formDefinition = result;
});

return this._buildForm(formDefinition);
}

_buildForm(formDefinition) {
const form = new FormGroup();

if (formDefinition.form.group) {
this._processFormGroups(formDefinition.form.group, form);
}

if (formDefinition.form.field) {
this._processFormFields(formDefinition.form.field, form);
}

if (formDefinition.form.include) {
this._processIncludes(formDefinition.form.include, form);
}

form.clearFormElement = () => {};

return form;
}

_processFormGroups(groups, form) {
groups.forEach((group) => {
const formGroup = new FormGroup();

group.field.forEach((field) => {
formGroup[field.$.formid] = new FormField(field);
});

if (group.include) {
group.include.forEach((include) => {
formGroup[include.$.formid] = this.getForm(include.$.name);
});
}

form[group.$.formid] = formGroup;
});
}

_processFormFields(fields, form) {
fields.forEach((field) => {
form[field.$.formid] = new FormField(field);
});
}

_processIncludes(includes, form) {
includes.forEach((include) => {
form[include.$.formid] = this.getForm(include.$.name);
});
}
}

module.exports = new FormsLoader();
3 changes: 3 additions & 0 deletions server/src/mocks/dw/web/FormElement.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class FormElement {}

module.exports = FormElement;
68 changes: 28 additions & 40 deletions server/src/mocks/dw/web/FormField.js
Original file line number Diff line number Diff line change
@@ -1,42 +1,30 @@
var _super = require('dw/web/FormElement');

var FormField = function () {};

FormField.prototype = new _super();

FormField.prototype.getValue = function () {};
FormField.prototype.getType = function () {};
FormField.prototype.setValue = function () {};
FormField.prototype.getDescription = function () {};
FormField.prototype.getOptions = function () {};
FormField.prototype.getError = function () {};
FormField.prototype.getMaxValue = function () {};
FormField.prototype.getRegEx = function () {};
FormField.prototype.getHtmlValue = function () {};
FormField.prototype.setHtmlValue = function () {};
FormField.prototype.setOptions = function () {};
FormField.prototype.getSelectedOption = function () {};
FormField.prototype.getSelectedOptionObject = function () {};
FormField.prototype.isChecked = function () {};
FormField.prototype.isSelected = function () {};
FormField.prototype.isMandatory = function () {};
FormField.prototype.getMinLength = function () {};
FormField.prototype.getMaxLength = function () {};
FormField.prototype.getMinValue = function () {};
FormField.prototype.getLabel = function () {};
FormField.prototype.value = '';
FormField.prototype.type = null;
FormField.prototype.description = null;
FormField.prototype.options = null;
FormField.prototype.error = null;
FormField.prototype.maxValue = null;
FormField.prototype.regEx = null;
FormField.prototype.htmlValue = null;
FormField.prototype.selectedOption = null;
FormField.prototype.selectedOptionObject = null;
FormField.prototype.minLength = null;
FormField.prototype.maxLength = null;
FormField.prototype.minValue = null;
FormField.prototype.label = null;
const FormElement = require('dw/web/FormElement');

class FormField extends FormElement {
_value = '';

options = [];

constructor(field) {
super();

if (field.options) {
this.options = this._processOptions(field.options[0].option);
}
}

get value() {
return this._value;
}

set value(value) {
this._value = value;
}

_processOptions(options) {
options.optionsCount = options.length;
return options;
}
}

module.exports = FormField;
5 changes: 5 additions & 0 deletions server/src/mocks/dw/web/FormGroup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
var FormElement = require('dw/web/FormElement');

class FormGroup extends FormElement {}

module.exports = FormGroup;
114 changes: 4 additions & 110 deletions server/src/server/Session.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
const FormField = require('dw/web/FormField');
const FormGroup = require('dw/web/FormGroup');
const FormsLoader = require('../forms/FormsLoader');

class Session {
_locale = 'default';

Expand All @@ -18,116 +20,7 @@ class Session {
}

get forms() {
return {
profile: {
customer: (() => {
const group = new FormGroup();

group.email = new FormField();
group.emailconfirm = new FormField();
group.firstname = new FormField();
group.lastname = new FormField();
group.phone = new FormField();

return group;
})(),
login: (() => {
const group = new FormGroup();

group.password = new FormField();
group.passwordconfirm = new FormField();

group.newpasswords = new FormGroup();
group.newpasswords.newpassword = new FormField();
group.newpasswords.newpasswordconfirm = new FormField();
group.currentpassword = new FormField();

return group;
})(),
clearFormElement() {},
},
newPasswords: {
newpassword: new FormField(),
newpasswordconfirm: new FormField(),
clearFormElement() {},
},
address: {
clearFormElement() {},
},
shipping: {
shippingAddress: (() => {
const group = new FormGroup();

group.addressFields = new FormGroup();
group.addressFields.firstName = new FormField();
group.addressFields.lastName = new FormField();
group.addressFields.address1 = new FormField();
group.addressFields.address2 = new FormField();
group.addressFields.city = new FormField();
group.addressFields.postalCode = new FormField();
group.addressFields.country = new FormField();
group.addressFields.phone = new FormField();

return group;
})(),
clearFormElement() {},
},
coCustomer: {
email: new FormField(),
clearFormElement() {},
},
coRegisteredCustomer: {
email: new FormField(),
clearFormElement() {},
},
billing: {
addressFields: (() => {
const group = new FormGroup();

group.firstName = new FormField();
group.lastName = new FormField();
group.address1 = new FormField();
group.address2 = new FormField();
group.city = new FormField();
group.postalCode = new FormField();
group.country = new FormField();
group.phone = new FormField();

return group;
})(),
contactInfoFields: (() => {
const group = new FormGroup();

group.firstName = new FormField();
group.lastName = new FormField();
group.address1 = new FormField();
group.address2 = new FormField();
group.city = new FormField();
group.postalCode = new FormField();
group.country = new FormField();
group.phone = new FormField();

return group;
})(),
paymentMethod: new FormField(),
clearFormElement() {},
},
creditCard: {
expirationMonth: (() => {
const f = new FormField();

f.options = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
f.options.optionsCount = 12;

return f;
})(),
cardOwner: new FormField(),
cardNumber: new FormField(),
cardType: new FormField(),
expirationYear: new FormField(),
clearFormElement() {},
},
};
return this._formsAccessor;
}

get sourceCodeInfo() {
Expand Down Expand Up @@ -175,6 +68,7 @@ class Session {

constructor(privacyCache) {
this.privacyCache = privacyCache;
this._formsAccessor = FormsLoader.getAccessor();
}
}

Expand Down
34 changes: 34 additions & 0 deletions server/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,39 @@ function locateTemplate(templateName, locale) {
throw new Error(`Template "${templateName}" (locale: ${langCode}) not found`);
}

/**
* Locates form definition file respecting the locale
* @param templateName {string}
* @param locale {{ id: string }}
*/
function locateFormDefinitionFile(formName, locale) {
if (!formName.endsWith('.xml')) {
formName += '.xml';
}

const langCode = locale.id;

const localeFormFile = locateSingleFileInCartridges(
`forms/${langCode}/${formName}`
);

if (localeFormFile) {
return localeFormFile;
}

const defaultFormFile = locateSingleFileInCartridges(
`forms/default/${formName}`
);

if (defaultFormFile) {
return defaultFormFile;
}

throw new Error(
`Form definition file for form "${formName}" (locale: ${langCode}) not found`
);
}

/**
* Locates all files according to cartridge path.
* Ignores non-existing cartridges.
Expand Down Expand Up @@ -95,4 +128,5 @@ module.exports = {
locateSingleFileInCartridges,
locateAllFilesInCartridges,
locateTemplate,
locateFormDefinitionFile,
};
Loading

0 comments on commit ad031ff

Please sign in to comment.