-
+
{% if (!instance.hasRemoveButtons || instance.hasRemoveButtons()) { %}
{% } %}
@@ -110,7 +117,7 @@ export default class EditGridComponent extends NestedArrayComponent {
{% if (!instance.options.readOnly && !instance.disabled) { %}
-
+
{% if (!instance.hasRemoveButtons || instance.hasRemoveButtons()) { %}
{% } %}
@@ -510,13 +517,10 @@ export default class EditGridComponent extends NestedArrayComponent {
action: () => {
this.editRow(rowIndex).then(() => {
if (this.component.rowDrafts) {
- this.validateRow(editRow, false);
-
- const hasErrors = editRow.errors && !!editRow.errors.length;
- const shouldShowRowErrorsAlert = this.component.modal && hasErrors && this.root?.submitted;
-
+ const errors = this.validateRow(editRow, false);
+ const shouldShowRowErrorsAlert = this.component.modal && errors.length && this.root?.submitted;
if (shouldShowRowErrorsAlert) {
- this.alert.showErrors(editRow.errors, false);
+ this.alert.showErrors(errors, false);
editRow.alerts = true;
}
}
@@ -725,7 +729,7 @@ export default class EditGridComponent extends NestedArrayComponent {
component: this.component,
row: editRow,
});
- this.checkRow('checkData', null, {}, editRow.data, editRow.components);
+ this.processRow('checkData', null, {}, editRow.data, editRow.components);
if (this.component.modal) {
this.addRowModal(rowIndex);
}
@@ -779,13 +783,15 @@ export default class EditGridComponent extends NestedArrayComponent {
if (!this.component.rowDrafts) {
editRow.components.forEach((comp) => comp.setPristine(false));
}
- if (this.validateRow(editRow, true) || this.component.rowDrafts) {
+
+ const errors = this.validateRow(editRow, true);
+ if (!errors.length || this.component.rowDrafts) {
editRow.willBeSaved = true;
dialog.close();
this.saveRow(rowIndex, true);
}
else {
- this.alert.showErrors(editRow.errors, false);
+ this.alert.showErrors(errors, false);
editRow.alerts = true;
}
},
@@ -937,10 +943,10 @@ export default class EditGridComponent extends NestedArrayComponent {
editRow.components.forEach((comp) => comp.setPristine(false));
}
- const isRowValid = this.validateRow(editRow, true);
+ const errors = this.validateRow(editRow, true);
if (!this.component.rowDrafts) {
- if (!isRowValid) {
+ if (errors.length) {
return false;
}
}
@@ -968,7 +974,7 @@ export default class EditGridComponent extends NestedArrayComponent {
}
}
- editRow.state = this.component.rowDrafts && !isRowValid ? EditRowState.Draft : EditRowState.Saved;
+ editRow.state = this.component.rowDrafts && errors.length ? EditRowState.Draft : EditRowState.Saved;
editRow.backup = null;
this.updateValue();
@@ -1096,22 +1102,12 @@ export default class EditGridComponent extends NestedArrayComponent {
const editRow = this.editRows[rowIndex];
- if (editRow?.alerts) {
- this.checkData(null, {
- ...flags,
- changed,
- rowIndex,
- }, this.data);
- }
- else if (editRow) {
- // If drafts allowed, perform validation silently if there was no attempt to submit a form
- const silentCheck = this.component.rowDrafts && !this.shouldValidateDraft(editRow);
-
- this.checkRow('checkData', null, {
+ if (editRow) {
+ this.processRow('checkData', null, {
...flags,
changed,
- silentCheck
- }, editRow.data, editRow.components, silentCheck);
+ }, editRow.data, editRow.components);
+ this.validateRow(editRow, false);
}
if (this.variableTypeComponentsIndexes.length) {
@@ -1137,6 +1133,12 @@ export default class EditGridComponent extends NestedArrayComponent {
return this.editRows.some(row => this.isOpen(row));
}
+ getAttachedData(data = null) {
+ const ourData = fastCloneDeep(data || this._data || this.rootValue);
+ _.set(ourData, this.key, this.editRows.map((row) => row.data));
+ return ourData;
+ }
+
shouldValidateDraft(editRow) {
// Draft rows should be validated only when there was an attempt to submit a form
return (editRow.state === EditRowState.Draft &&
@@ -1148,97 +1150,106 @@ export default class EditGridComponent extends NestedArrayComponent {
shouldValidateRow(editRow, dirty) {
return this.shouldValidateDraft(editRow) ||
+ editRow.state === EditRowState.New ||
editRow.state === EditRowState.Editing ||
editRow.alerts ||
dirty;
}
validateRow(editRow, dirty, forceSilentCheck) {
- let valid = true;
- const errorsSnapshot = [...this.errors];
-
+ editRow.errors = [];
if (this.shouldValidateRow(editRow, dirty)) {
- editRow.components.forEach(comp => {
- const silentCheck = (this.component.rowDrafts && !this.shouldValidateDraft(editRow)) || forceSilentCheck;
-
- valid &= comp.checkValidity(null, dirty, null, silentCheck);
- });
+ const silentCheck = (this.component.rowDrafts && !this.shouldValidateDraft(editRow)) || forceSilentCheck;
+ const rootValue = fastCloneDeep(this.rootValue);
+ const editGridValue = _.get(rootValue, this.path, []);
+ editGridValue[editRow.rowIndex] = editRow.data;
+ _.set(rootValue, this.path, editGridValue);
+ const validationProcessorProcess = (context) => this.validationProcessor(context, { dirty, silentCheck });
+ editRow.errors = processSync({
+ components: fastCloneDeep(this.component.components).map((component) => {
+ component.parentPath = `${this.path}[${editRow.rowIndex}]`;
+ return component;
+ }),
+ data: rootValue,
+ row: editRow.data,
+ process: 'validateRow',
+ instances: this.componentsMap,
+ scope: { errors: [] },
+ processors: [
+ {
+ process: validationProcessorProcess,
+ processSync: validationProcessorProcess
+ }
+ ]
+ }).errors;
}
+ // TODO: this is essentially running its own custom validation and should be moved into a validation rule
if (this.component.validate && this.component.validate.row) {
- valid = this.evaluate(this.component.validate.row, {
- valid,
+ const valid = this.evaluate(this.component.validate.row, {
+ valid: (editRow.length === 0),
row: editRow.data
}, 'valid', true);
if (valid.toString() !== 'true') {
- editRow.error = valid;
- valid = false;
- }
- else {
- editRow.error = null;
+ editRow.errors.push({
+ type: 'error',
+ rowError: true,
+ message: valid.toString()
+ });
}
if (valid === null) {
- valid = `Invalid row validation for ${this.key}`;
+ editRow.errors.push({
+ type: 'error',
+ message: `Invalid row validation for ${this.key}`
+ });
}
}
- editRow.errors = !valid ? this.errors.filter((err) => !errorsSnapshot.includes(err)) : null;
-
if (!this.component.rowDrafts || this.root?.submitted) {
- this.showRowErrorAlerts(editRow, !!valid);
+ this.showRowErrorAlerts(editRow, editRow.errors);
}
- return !!valid;
+ return editRow.errors;
}
- showRowErrorAlerts(editRow, valid) {
+ showRowErrorAlerts(editRow, errors) {
if (editRow.alerts) {
if (this.alert) {
- if (editRow.errors?.length && !valid) {
- this.alert.showErrors(editRow.errors, false);
+ if (errors.length) {
+ this.alert.showErrors(errors, false);
editRow.alerts = true;
}
else {
this.alert.clear();
+ this.alert = null;
}
}
}
}
- checkValidity(data, dirty, row, silentCheck) {
- data = data || this.rootValue;
- row = row || this.data;
-
- if (!this.checkCondition(row, data)) {
- this.setCustomValidity('');
- return true;
- }
-
- return this.checkComponentValidity(data, dirty, row, { silentCheck });
+ /**
+ * Return that this component processes its own validation.
+ */
+ get processOwnValidation() {
+ return true;
}
- checkComponentValidity(data, dirty, row, options = {}) {
+ checkComponentValidity(data, dirty, row, options = {}, errors = []) {
const { silentCheck } = options;
- const errorsLength = this.errors.length;
- const superValid = super.checkComponentValidity(data, dirty, row, options);
+ const superValid = super.checkComponentValidity(data, dirty, row, options, errors);
// If super tells us that component invalid and there is no need to update alerts, just return false
if (!superValid && (!this.alert && !this.hasOpenRows())) {
return false;
}
- if (this.shouldSkipValidation(data, dirty, row)) {
- return true;
- }
-
- let rowsValid = true;
let rowsEditing = false;
-
+ const allRowErrors = [];
this.editRows.forEach((editRow, index) => {
// Trigger all errors on the row.
- const rowValid = this.validateRow(editRow, dirty, silentCheck);
-
- rowsValid &= rowValid;
+ const rowErrors = this.validateRow(editRow, dirty, silentCheck);
+ errors.push(...rowErrors);
+ allRowErrors.push(...rowErrors);
if (this.rowRefs) {
const rowContainer = this.rowRefs[index];
@@ -1246,9 +1257,10 @@ export default class EditGridComponent extends NestedArrayComponent {
if (rowContainer) {
const errorContainer = rowContainer.querySelector('.editgrid-row-error');
- if (!rowValid && errorContainer && (!this.component.rowDrafts || this.shouldValidateDraft(editRow))) {
+ if (rowErrors.length && errorContainer && (!this.component.rowDrafts || this.shouldValidateDraft(editRow))) {
+ const rowError = rowErrors.find(error => error.rowError);
this.addClass(errorContainer, 'help-block' );
- errorContainer.textContent = this.t(this.errorMessage('invalidRowError'));
+ errorContainer.textContent = this.t(rowError ? rowError.message : this.errorMessage('invalidRowError'));
}
else if (errorContainer) {
errorContainer.textContent = '';
@@ -1259,32 +1271,45 @@ export default class EditGridComponent extends NestedArrayComponent {
rowsEditing |= (dirty && this.isOpen(editRow));
});
- if (!rowsValid) {
- if (!silentCheck && (!this.component.rowDrafts || this.root?.submitted)) {
+ if (allRowErrors.length) {
+ if (!silentCheck && (dirty || this.dirty) && (!this.component.rowDrafts || this.root?.submitted)) {
this.setCustomValidity(this.t(this.errorMessage('invalidRowsError')), dirty);
- // Delete this class, because otherwise all the components inside EditGrid will has red border even if they are valid
this.removeClass(this.element, 'has-error');
}
return false;
}
- else if (rowsEditing && this.saveEditMode) {
+ else if (rowsEditing && this.saveEditMode && !this.component.openWhenEmpty) {
this.setCustomValidity(this.t(this.errorMessage('unsavedRowsError')), dirty);
return false;
}
- const message = this.invalid || this.invalidMessage(data, dirty);
- if (this.errors?.length !== errorsLength && this.root?.submitted && !message) {
- this.setCustomValidity(message, dirty);
- this.root.showErrors();
+ const message = this.invalid || this.invalidMessage(data, dirty, false, row);
+ if (allRowErrors.length && this.root?.submitted && !message) {
+ this._errors = this.setCustomValidity(message, dirty);
+ errors.push(...this._errors);
+ this.root.showErrors([message]);
}
else {
- this.setCustomValidity(message, dirty);
+ this._errors = this.setCustomValidity(message, dirty);
+ errors.push(...this._errors);
}
return superValid;
}
+ setRowInvalid(ref, index) {
+ const editRow = this.editRows[index];
+ const errorContainer = ref.querySelector('.editgrid-row-error');
+ if (errorContainer && (!this.component.rowDrafts || this.shouldValidateDraft(editRow))) {
+ this.addClass(errorContainer, 'help-block' );
+ errorContainer.textContent = this.t(this.errorMessage('invalidRowError'));
+ }
+ else if (errorContainer) {
+ errorContainer.textContent = '';
+ }
+ }
+
changeState(changed, flags) {
- if (changed || (flags.resetValue && this.component.modalEdit)) {
+ if (this.visible && (changed || (flags.resetValue && this.component.modalEdit))) {
this.rebuild();
}
else {
@@ -1307,8 +1332,7 @@ export default class EditGridComponent extends NestedArrayComponent {
}
const changed = this.hasChanged(value, this.dataValue);
- flags.noValidate = !changed;
- if (this.parent && !(this.options.server && !this.parent.parentVisible)) {
+ if (this.parent && !this.options.server) {
this.parent.checkComponentConditions();
}
this.dataValue = value;
@@ -1320,7 +1344,7 @@ export default class EditGridComponent extends NestedArrayComponent {
this.restoreRowContext(editRow, flags);
editRow.state = EditRowState.Saved;
editRow.backup = null;
- editRow.error = null;
+ editRow.errors = [];
}
else {
this.editRows[rowIndex] = {
@@ -1328,7 +1352,7 @@ export default class EditGridComponent extends NestedArrayComponent {
data: row,
state: EditRowState.Saved,
backup: null,
- error: null,
+ errors: [],
};
}
});
@@ -1343,7 +1367,10 @@ export default class EditGridComponent extends NestedArrayComponent {
this.openWhenEmpty();
this.updateOnChange(flags, changed);
- this.checkData();
+ // do not call checkData with server option, it is called when change is triggered in updateOnChange
+ if (!this.options.server) {
+ this.checkData();
+ }
this.changeState(changed, flags);
diff --git a/src/components/editgrid/EditGrid.unit.js b/src/components/editgrid/EditGrid.unit.js
index d4f83630ec..90ddc9a7df 100644
--- a/src/components/editgrid/EditGrid.unit.js
+++ b/src/components/editgrid/EditGrid.unit.js
@@ -21,8 +21,10 @@ import {
compOpenWhenEmpty,
compWithCustomDefaultValue,
} from './fixtures';
+import formsWithEditGridAndConditions from './fixtures/formsWithEditGridAndConditions';
import ModalEditGrid from '../../../test/forms/modalEditGrid';
+import EditGridOpenWhenEmpty from '../../../test/forms/editGridOpenWhenEmpty';
import Webform from '../../Webform';
import { displayAsModalEditGrid } from '../../../test/formtest';
import { Formio } from '../../Formio';
@@ -95,7 +97,7 @@ describe('EditGrid Component', () => {
Harness.testElements(component, 'div.editRow', 0);
Harness.testElements(component, 'div.removeRow', 0);
assert.equal(component.refs[`${component.editgridKey}-addRow`].length, 1);
- assert(component.checkValidity(component.getValue()), 'Item should be valid');
+ assert(component.checkValidity(), 'Item should be valid');
});
});
@@ -125,7 +127,7 @@ describe('EditGrid Component', () => {
Harness.testInnerHtml(component, 'li.list-group-item:nth-child(2) div.row div:nth-child(2)', 'foo');
Harness.testInnerHtml(component, 'li.list-group-item:nth-child(3) div.row div:nth-child(1)', 'good');
Harness.testInnerHtml(component, 'li.list-group-item:nth-child(3) div.row div:nth-child(2)', 'bar');
- assert(component.checkValidity(component.getValue()), 'Item should be valid');
+ assert(component.checkValidity(), 'Item should be valid');
});
});
@@ -139,7 +141,7 @@ describe('EditGrid Component', () => {
Harness.clickElement(component, component.refs[`${component.editgridKey}-addRow`][0]);
Harness.testElements(component, 'li.list-group-item', 3);
Harness.testInnerHtml(component, 'li.list-group-header div.row div:nth-child(3)', '0');
- assert(!component.checkValidity(component.getValue(), true), 'Item should not be valid');
+ assert(!component.checkValidity(null, true), 'Item should not be valid');
});
});
@@ -168,7 +170,7 @@ describe('EditGrid Component', () => {
Harness.testInnerHtml(component, 'li.list-group-header div.row div:nth-child(3)', '3');
Harness.testInnerHtml(component, 'li.list-group-item:nth-child(4) div.row div:nth-child(1)', 'good');
Harness.testInnerHtml(component, 'li.list-group-item:nth-child(4) div.row div:nth-child(2)', 'baz');
- assert(component.checkValidity(component.getValue()), 'Item should be valid');
+ assert(component.checkValidity(), 'Item should be valid');
});
});
@@ -195,7 +197,7 @@ describe('EditGrid Component', () => {
Harness.testElements(component, 'li.list-group-item', 3);
Harness.testInnerHtml(component, 'li.list-group-header div.row div:nth-child(3)', '2');
assert.equal(component.editRows.length, 2);
- assert(component.checkValidity(component.getValue(), true), 'Item should be valid');
+ assert(component.checkValidity(null, true), 'Item should be valid');
});
});
@@ -222,7 +224,7 @@ describe('EditGrid Component', () => {
Harness.testInnerHtml(component, 'li.list-group-item:nth-child(2) div.row div:nth-child(2)', 'foo');
Harness.testInnerHtml(component, 'li.list-group-item:nth-child(3) div.row div:nth-child(1)', 'good');
Harness.testInnerHtml(component, 'li.list-group-item:nth-child(3) div.row div:nth-child(2)', 'baz');
- assert(component.checkValidity(component.getValue(), true), 'Item should be valid');
+ assert(component.checkValidity(null, true), 'Item should be valid');
});
});
@@ -243,7 +245,7 @@ describe('EditGrid Component', () => {
Harness.getInputValue(component, 'data[editgrid][1][field2]', 'bar');
Harness.testElements(component, 'div.editgrid-actions button.btn-primary', 1);
Harness.testElements(component, 'div.editgrid-actions button.btn-danger', 1);
- assert(!component.checkValidity(component.getValue(), true), 'Item should not be valid');
+ assert(!component.checkValidity(null, true), 'Item should not be valid');
});
});
@@ -266,7 +268,7 @@ describe('EditGrid Component', () => {
Harness.testInnerHtml(component, 'li.list-group-header div.row div:nth-child(3)', '2');
Harness.testInnerHtml(component, 'li.list-group-item:nth-child(3) div.row div:nth-child(1)', 'good');
Harness.testInnerHtml(component, 'li.list-group-item:nth-child(3) div.row div:nth-child(2)', 'baz');
- assert(component.checkValidity(component.getValue(), true), 'Item should be valid');
+ assert(component.checkValidity(null, true), 'Item should be valid');
});
});
@@ -289,10 +291,11 @@ describe('EditGrid Component', () => {
Harness.testInnerHtml(component, 'li.list-group-header div.row div:nth-child(3)', '2');
Harness.testInnerHtml(component, 'li.list-group-item:nth-child(3) div.row div:nth-child(1)', 'good');
Harness.testInnerHtml(component, 'li.list-group-item:nth-child(3) div.row div:nth-child(2)', 'bar');
- assert(component.checkValidity(component.getValue(), true), 'Item should be valid');
+ assert(component.checkValidity(null, true), 'Item should be valid');
});
});
+ // TODO: find out if this is deprecated in the new (3.x and above) versions of the renderer, if so ditch this test
it('Should show error messages for existing data in rows', () => {
return Harness.testCreate(EditGridComponent, comp1).then((component) => {
Harness.testSetGet(component, [
@@ -309,9 +312,9 @@ describe('EditGrid Component', () => {
field2: 'baz'
}
]);
+ assert(!component.checkValidity(null, true), 'Item should not be valid');
Harness.testInnerHtml(component, 'li.list-group-item:nth-child(2) div.has-error div.editgrid-row-error', 'Must be good');
Harness.testInnerHtml(component, 'li.list-group-item:nth-child(4) div.has-error div.editgrid-row-error', 'Must be good');
- assert(!component.checkValidity(component.getValue(), true), 'Item should not be valid');
});
});
@@ -321,20 +324,20 @@ describe('EditGrid Component', () => {
Harness.clickElement(component, 'div.editgrid-actions button.btn-primary');
Harness.getInputValue(component, 'data[editgrid][0][field1]', '');
Harness.getInputValue(component, 'data[editgrid][0][field2]', '');
- assert(!component.checkValidity(component.getValue(), true), 'Item should not be valid');
+ assert(!component.checkValidity(null, true), 'Item should not be valid');
Harness.setInputValue(component, 'data[editgrid][0][field2]', 'baz');
Harness.clickElement(component, 'div.editgrid-actions button.btn-primary');
Harness.getInputValue(component, 'data[editgrid][0][field1]', '');
Harness.getInputValue(component, 'data[editgrid][0][field2]', 'baz');
- assert(!component.checkValidity(component.getValue(), true), 'Item should not be valid');
+ assert(!component.checkValidity(null, true), 'Item should not be valid');
Harness.setInputValue(component, 'data[editgrid][0][field1]', 'bad');
Harness.clickElement(component, 'div.editgrid-actions button.btn-primary');
Harness.getInputValue(component, 'data[editgrid][0][field1]', 'bad');
Harness.getInputValue(component, 'data[editgrid][0][field2]', 'baz');
- assert(!component.checkValidity(component.getValue(), true), 'Item should not be valid');
+ assert(!component.checkValidity(null, true), 'Item should not be valid');
Harness.setInputValue(component, 'data[editgrid][0][field1]', 'good');
Harness.clickElement(component, 'div.editgrid-actions button.btn-primary');
- assert(component.checkValidity(component.getValue(), true), 'Item should be valid');
+ assert(component.checkValidity(null, true), 'Item should be valid');
Harness.testInnerHtml(component, 'li.list-group-header div.row div:nth-child(3)', '1');
Harness.testInnerHtml(component, 'li.list-group-item:nth-child(2) div.row div:nth-child(1)', 'good');
Harness.testInnerHtml(component, 'li.list-group-item:nth-child(2) div.row div:nth-child(2)', 'baz');
@@ -354,13 +357,13 @@ describe('EditGrid Component', () => {
}
]);
Harness.clickElement(component, 'li.list-group-item:nth-child(3) div.editRow');
- assert(!component.checkValidity(component.getValue(), true), 'Item should not be valid');
+ assert(!component.checkValidity(null, true), 'Item should not be valid');
Harness.clickElement(component, 'div.editgrid-actions button.btn-primary');
- assert(component.checkValidity(component.getValue(), true), 'Item should be valid');
+ assert(component.checkValidity(null, true), 'Item should be valid');
Harness.clickElement(component, 'li.list-group-item:nth-child(3) div.editRow');
- assert(!component.checkValidity(component.getValue(), true), 'Item should not be valid');
+ assert(!component.checkValidity(null, true), 'Item should not be valid');
Harness.clickElement(component, 'div.editgrid-actions button.btn-danger');
- assert(component.checkValidity(component.getValue(), true), 'Item should be valid');
+ assert(component.checkValidity(null, true), 'Item should be valid');
});
});
@@ -542,7 +545,7 @@ describe('EditGrid Component', () => {
component.addRow();
Harness.clickElement(component, '[ref="editgrid-editGrid1-saveRow"]');
assert.deepEqual(component.dataValue, [{ textField: '' }]);
- const isInvalid = !component.checkValidity(component.dataValue, true);
+ const isInvalid = !component.checkValidity(null, true);
assert(isInvalid, 'Item should not be valid');
assert(component.editRows[0].state === 'draft', 'Row should be saved as draft if it has errors');
done();
@@ -622,7 +625,6 @@ describe('EditGrid Component', () => {
setTimeout(() => {
const alert = dialog.querySelector('.alert.alert-danger');
- assert.equal(form.errors.length, 0, 'Should not add new errors when drafts are enabled');
assert(!alert, 'Should not show an error alert when drafts are enabled and form is not submitted');
const textField = editRow.components[0].getComponent('textField');
@@ -641,17 +643,14 @@ describe('EditGrid Component', () => {
setTimeout(() => {
assert.equal(textField.dataValue, '');
- assert.equal(editGrid.editRows[0].errors.length, 0, 'Should not add error to components inside draft row');
-
const textFieldComponent = textField.element;
assert(textFieldComponent.className.includes('has-error'), 'Should add error class to component even when drafts enabled if the component is not pristine');
-
document.innerHTML = '';
done();
}, 300);
}, 300);
}, 150);
- }, 100);
+ }, 800);
}, 100);
}).catch(done)
.finally(() => {
@@ -678,11 +677,9 @@ describe('EditGrid Component', () => {
setTimeout(() => {
// 3. Submit the form
- Harness.dispatchEvent('click', form.element, '[name="data[submit]"]');
-
- setTimeout(() => {
- assert.equal(editGrid.errors.length, 3, 'Should be validated after an attempt to submit');
- assert.equal(editGrid.editRows[0].errors.length, 2, 'Should dd errors to the row after an attempt to submit');
+ form.submit().finally(() => {
+ assert.equal(form.errors.length, 2, 'Should be validated after an attempt to submit');
+ assert.equal(editGrid.editRows[0].errors.length, 2, 'Should add errors to the row after an attempt to submit');
const rows = editGrid.element.querySelectorAll('[ref="editgrid-editGrid-row"]');
const firstRow = rows[0];
Harness.dispatchEvent('click', firstRow, '.editRow');
@@ -759,7 +756,7 @@ describe('EditGrid Component', () => {
}, 300);
}, 300);
}, 450);
- }, 250);
+ });
}, 100);
}, 100);
}).catch(done)
@@ -1212,7 +1209,7 @@ describe('EditGrid Component', () => {
editGrid.refs['editgrid-editGrid-saveRow'][0].dispatchEvent(clickEvent);
setTimeout(() => {
- assert.equal(!!firstRowTextField.error, true);
+ assert.equal(!!firstRowTextField.errors, true);
assert.equal(editGrid.editRows[0].errors.length, 1);
assert.equal(editGrid.editRows[0].state, 'new');
@@ -1224,25 +1221,167 @@ describe('EditGrid Component', () => {
}).catch(done);
});
- it('Should render form with a submission in a draft-state without validation errors', (done) => {
+ it('Should submit a form with a submission in a draft-state without validation errors', (done) => {
const form = _.cloneDeep(comp13);
const element = document.createElement('div');
-
Formio.createForm(element, form).then(form => {
- form.submission = {
+ form.nosubmit = true;
+ form.setSubmission({
+ state: 'draft',
data: {
'container': {
'textField': '',
},
'editGrid': []
}
+ }).then(() => {
+ form.submitForm().then((event) => {
+ // It should allow the submission in the renderer.
+ assert.equal(event.submission.state, 'draft');
+ done();
+ }).catch((err) => done(err));
+ }).catch(done);
+ }).catch(done);
+ });
+
+ it('Should keep value for conditional editGrid on setValue when server option is provided', (done) => {
+ const element = document.createElement('div');
+
+ Formio.createForm(element, formsWithEditGridAndConditions.form1, { server: true }).then(form => {
+ const formData = {
+ checkbox: true,
+ radio: 'yes',
+ editGrid: [
+ { textField: 'test', number: 4 },
+ { textField: 'test1', number: 5 },
+ ],
};
+ form.setValue({ data: _.cloneDeep(formData) });
+
setTimeout(() => {
- const editGrid = form.getComponent(['editGrid']);
- assert.equal(editGrid.errors.length, 0);
+ const editGrid = form.getComponent('editGrid');
+ assert.deepEqual(editGrid.dataValue, formData.editGrid);
+
done();
- }, 100);
+ }, 500);
+ }).catch(done);
+ });
+
+ it('Should set value for conditional editGrid inside editGrid on event when form is not pristine ', (done) => {
+ const element = document.createElement('div');
+
+ Formio.createForm(element, formsWithEditGridAndConditions.form2).then(form => {
+ form.setPristine(false);
+ const editGrid1 = form.getComponent('editGrid1');
+ editGrid1.addRow();
+
+ setTimeout(() => {
+ const btn = editGrid1.getComponent('setPanelValue')[0];
+ const clickEvent = new Event('click');
+ btn.refs.button.dispatchEvent(clickEvent);
+ setTimeout(() => {
+ const conditionalEditGrid = editGrid1.getComponent('editGrid')[0];
+ assert.deepEqual(conditionalEditGrid.dataValue, [{ textField:'testyyyy' }]);
+ assert.equal(conditionalEditGrid.editRows.length, 1);
+ done();
+ }, 500);
+ }, 300);
+ }).catch(done);
+ });
+
+ it('Should keep value for conditional editGrid in tabs on setValue when server option is provided', (done) => {
+ const element = document.createElement('div');
+
+ Formio.createForm(element, formsWithEditGridAndConditions.form3, { server: true }).then(form => {
+ const formData = {
+ affectedRiskTypes: {
+ creditRisk: false,
+ marketRisk: true,
+ operationalRisk: false,
+ counterpartyCreditRisk: false,
+ creditValuationRiskAdjustment: false,
+ },
+ rwaImpact: 'yes',
+ submit: true,
+ mr: {
+ quantitativeInformation: {
+ cva: 'yes',
+ sameRiskCategories: false,
+ impactsPerEntity: [{ number: 123 }],
+ sameImpactAcrossEntities: false,
+ },
+ },
+ euParentInstitution: 'EUParent',
+ };
+
+ form.setValue({ data: _.cloneDeep(formData) });
+
+ setTimeout(() => {
+ const editGrid = form.getComponent('impactsPerEntity');
+ assert.deepEqual(editGrid.dataValue, formData.mr.quantitativeInformation.impactsPerEntity);
+ assert.deepEqual(editGrid.editRows.length, 1);
+
+ done();
+ }, 500);
+ }).catch(done);
+ });
+
+ it('Should calculate editGrid value when calculateOnServer is enabled and server option is passed', (done) => {
+ const element = document.createElement('div');
+
+ Formio.createForm(element, formsWithEditGridAndConditions.form4, { server: true }).then(form => {
+ const editGrid = form.getComponent('editGrid');
+ assert.deepEqual(editGrid.dataValue, [{ textArea: 'test' }]);
+ assert.deepEqual(editGrid.editRows.length, 1);
+ done();
+ }).catch(done);
+ });
+
+ it('Should keep value for conditional editGrid deeply nested in panels and containers on setValue when server option is provided', (done) => {
+ const element = document.createElement('div');
+
+ Formio.createForm(element, formsWithEditGridAndConditions.form5, { server: true }).then(form => {
+ const formData = {
+ generalInformation: {
+ listSupervisedEntitiesCovered: [
+ { id: 6256, longName: 'Bank_DE', leiCode: 'LEI6256', countryCode: 'DE' },
+ ],
+ deSpecific: {
+ criticalPartsToBeOutsourcedSuboutsourcer: 'yes',
+ suboutsourcers: [
+ { nameSuboutsourcer: 'test' },
+ { nameSuboutsourcer: 'test 1' },
+ ],
+ },
+ },
+ };
+
+ form.setValue({ data: _.cloneDeep(formData) });
+
+ setTimeout(() => {
+ const editGrid = form.getComponent('suboutsourcers');
+ assert.deepEqual(editGrid.dataValue, formData.generalInformation.deSpecific.suboutsourcers);
+ assert.deepEqual(editGrid.editRows.length, 2);
+
+ done();
+ }, 500);
+ }).catch(done);
+ });
+
+ it('Should calculate editGrid value when condition is met in advanced logic', (done) => {
+ const element = document.createElement('div');
+
+ Formio.createForm(element, formsWithEditGridAndConditions.form6).then(form => {
+ form.getComponent('textField').setValue('show');
+
+ setTimeout(() => {
+ const editGrid = form.getComponent('editGrid');
+ assert.deepEqual(editGrid.dataValue, [{ number:1, textArea: 'test' }, { number:2, textArea: 'test2' }]);
+ assert.deepEqual(editGrid.editRows.length, 2);
+
+ done();
+ }, 300);
}).catch(done);
});
});
@@ -1382,4 +1521,44 @@ describe('EditGrid Open when Empty', () => {
})
.catch(done);
});
+
+ it('Should submit form with empty rows when submit button is pressed and no rows are saved', (done) => {
+ const formElement = document.createElement('div');
+ const form = new Webform(formElement);
+
+ form.setForm(compOpenWhenEmpty).then(() => {
+ const editGrid = form.components[0];
+
+ setTimeout(() => {
+ Harness.dispatchEvent('click', form.element, '[name="data[submit]"]');
+ setTimeout(() => {
+ const editRow = editGrid.editRows[0];
+ assert.equal(editRow.errors.length, 0, 'Should not be any errors on open row');
+ assert.equal(form.submission.state, 'submitted', 'Form should be submitted');
+ done();
+ }, 450);
+ }, 100);
+ }).catch(done);
+ });
+
+ it('Should not submit form if any row inputs are set as required', (done) => {
+ const formElement = document.createElement('div');
+ const form = new Webform(formElement);
+
+ form.setForm(EditGridOpenWhenEmpty).then(() => {
+ const editGrid = form.components[0];
+
+ setTimeout(() => {
+ Harness.dispatchEvent('click', form.element, '[name="data[submit]"]');
+ setTimeout(() => {
+ assert(!form.submission.state, 'Form should not be submitted');
+ const editRow = editGrid.editRows[0];
+ assert.equal(editRow.errors.length, 1, 'Should show error on row');
+ const textField = editRow.components[0];
+ assert(textField.element.className.includes('formio-error-wrapper'), 'Should add error class to component');
+ done();
+ }, 450);
+ }, 100);
+ }).catch(done);
+ });
});
diff --git a/src/components/editgrid/fixtures/formsWithEditGridAndConditions.js b/src/components/editgrid/fixtures/formsWithEditGridAndConditions.js
new file mode 100644
index 0000000000..024f448bd4
--- /dev/null
+++ b/src/components/editgrid/fixtures/formsWithEditGridAndConditions.js
@@ -0,0 +1,942 @@
+const form1 = {
+ title: 'form1',
+ name: 'form1',
+ path: 'form1',
+ type: 'form',
+ display: 'form',
+ components: [
+ {
+ label: 'Checkbox',
+ tableView: false,
+ key: 'checkbox',
+ type: 'checkbox',
+ input: true,
+ },
+ {
+ collapsible: false,
+ key: 'panel',
+ conditional: {
+ show: true,
+ conjunction: 'all',
+ conditions: [
+ {
+ component: 'checkbox',
+ operator: 'isEqual',
+ value: true,
+ },
+ ],
+ },
+ type: 'panel',
+ label: 'Panel',
+ input: false,
+ tableView: false,
+ components: [
+ {
+ label: 'Radio',
+ optionsLabelPosition: 'right',
+ inline: false,
+ tableView: false,
+ values: [
+ {
+ label: 'yes',
+ value: 'yes',
+ shortcut: '',
+ },
+ {
+ label: 'no',
+ value: 'no',
+ shortcut: '',
+ },
+ ],
+ key: 'radio',
+ type: 'radio',
+ input: true,
+ },
+ {
+ label: 'Edit Grid',
+ tableView: false,
+ rowDrafts: false,
+ key: 'editGrid',
+ conditional: {
+ show: true,
+ conjunction: 'all',
+ conditions: [
+ {
+ component: 'radio',
+ operator: 'isEqual',
+ value: 'yes',
+ },
+ ],
+ },
+ type: 'editgrid',
+ displayAsTable: false,
+ input: true,
+ components: [
+ {
+ label: 'Text Field',
+ applyMaskOn: 'change',
+ tableView: true,
+ key: 'textField',
+ type: 'textfield',
+ input: true,
+ },
+ {
+ label: 'Number',
+ applyMaskOn: 'change',
+ mask: false,
+ tableView: true,
+ delimiter: false,
+ requireDecimal: false,
+ inputFormat: 'plain',
+ truncateMultipleSpaces: false,
+ calculateValue: 'value = row.textField.length;',
+ key: 'number',
+ type: 'number',
+ input: true,
+ },
+ ],
+ },
+ ],
+ },
+ {
+ type: 'button',
+ label: 'Submit',
+ key: 'submit',
+ disableOnInvalid: true,
+ input: true,
+ tableView: false,
+ },
+ ],
+};
+
+const form2 = {
+ title: 'form2',
+ name: 'testyyy',
+ path: 'testyyy',
+ type: 'form',
+ display: 'form',
+ components: [
+ {
+ label: 'Text Area',
+ autoExpand: false,
+ tableView: true,
+ key: 'textArea',
+ type: 'textarea',
+ input: true,
+ },
+ {
+ label: 'Edit Grid',
+ tableView: false,
+ rowDrafts: false,
+ key: 'editGrid1',
+ type: 'editgrid',
+ displayAsTable: false,
+ input: true,
+ components: [
+ {
+ label: 'Set Panel Value',
+ action: 'custom',
+ showValidations: false,
+ tableView: false,
+ key: 'setPanelValue',
+ type: 'button',
+ custom:
+ "var rowIndex = instance.rowIndex;\nvar rowComponents = instance.parent?.editRows[rowIndex]?.components;\nvar panel = rowComponents?.find(comp => comp.component.key === 'panel');\npanel.setValue({radio: 'a', editGrid: [{textField:'testyyyy' }]});",
+ input: true,
+ },
+ {
+ collapsible: false,
+ key: 'panel',
+ type: 'panel',
+ label: 'Panel',
+ input: false,
+ tableView: false,
+ components: [
+ {
+ label: 'Radio',
+ optionsLabelPosition: 'right',
+ inline: false,
+ tableView: false,
+ values: [
+ {
+ label: 'a',
+ value: 'a',
+ shortcut: '',
+ },
+ {
+ label: 'b',
+ value: 'b',
+ shortcut: '',
+ },
+ ],
+ key: 'radio',
+ type: 'radio',
+ input: true,
+ },
+ {
+ title: 'Grid Panel',
+ collapsible: false,
+ key: 'panel1',
+ customConditional: "show = row.radio === 'a'",
+ type: 'panel',
+ label: 'Grid Panel',
+ input: false,
+ tableView: false,
+ components: [
+ {
+ label: 'Edit Grid',
+ openWhenEmpty: true,
+ disableAddingRemovingRows: true,
+ tableView: false,
+ rowDrafts: false,
+ key: 'editGrid',
+ conditional: {
+ show: true,
+ conjunction: 'all',
+ conditions: [
+ {
+ component: 'radio',
+ operator: 'isEqual',
+ value: 'a',
+ },
+ ],
+ },
+ type: 'editgrid',
+ displayAsTable: false,
+ input: true,
+ components: [
+ {
+ label: 'Text Field',
+ tableView: true,
+ key: 'textField',
+ type: 'textfield',
+ input: true,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ type: 'button',
+ label: 'Submit',
+ key: 'submit',
+ disableOnInvalid: true,
+ input: true,
+ tableView: false,
+ },
+ ],
+};
+
+const form3 = {
+ title: 'form3',
+ name: 'form3',
+ path: 'form3',
+ type: 'form',
+ display: 'form',
+ components: [
+ {
+ label: 'Tabs',
+ components: [
+ {
+ label: 'tab1',
+ key: 'generalInformationTab',
+ components: [
+ {
+ title: 'Fill first this tab, then the second one',
+ theme: 'primary',
+ collapsible: false,
+ key: 'riskInfo',
+ type: 'panel',
+ label: 'Risk Information',
+ tableView: false,
+ input: false,
+ components: [
+ {
+ label: 'Select the second option',
+ optionsLabelPosition: 'right',
+ tableView: false,
+ defaultValue: {
+ creditRisk: false,
+ marketRisk: false,
+ operationalRisk: false,
+ counterpartyCreditRisk: false,
+ creditValuationRiskAdjustment: false,
+ },
+ values: [
+ {
+ label: 'Do not select',
+ value: 'creditRisk',
+ shortcut: '',
+ },
+ {
+ label: 'Select this one',
+ value: 'marketRisk',
+ shortcut: '',
+ },
+ {
+ label: 'Do not select',
+ value: 'operationalRisk',
+ shortcut: '',
+ },
+ {
+ label: 'Do not select',
+ value: 'counterpartyCreditRisk',
+ shortcut: '',
+ },
+ {
+ label: 'Do not select',
+ value: 'creditValuationRiskAdjustment',
+ shortcut: '',
+ },
+ ],
+ key: 'affectedRiskTypes',
+ type: 'selectboxes',
+ input: true,
+ inputType: 'checkbox',
+ },
+ ],
+ },
+ {
+ title: '1.4 Details of change',
+ theme: 'primary',
+ collapsible: false,
+ key: 'changeInformationPanel',
+ type: 'panel',
+ label: 'Change information',
+ input: false,
+ tableView: false,
+ components: [
+ {
+ title: 'select options according to the label',
+ collapsible: false,
+ key: 'rwaImpactPanel',
+ type: 'panel',
+ label: 'Panel',
+ input: false,
+ tableView: false,
+ components: [
+ {
+ label: 'here select yes',
+ optionsLabelPosition: 'right',
+ customClass: 'tooltip-text-left',
+ inline: true,
+ tableView: true,
+ values: [
+ {
+ label: 'Yes',
+ value: 'yes',
+ shortcut: '',
+ },
+ {
+ label: 'No',
+ value: 'no',
+ shortcut: '',
+ },
+ ],
+ key: 'rwaImpact',
+ type: 'radio',
+ labelWidth: 100,
+ input: true,
+ },
+ {
+ label:
+ 'here select the first option, then go to the second tab',
+ optionsLabelPosition: 'right',
+ customClass: 'ml-3',
+ inline: false,
+ tableView: false,
+ values: [
+ {
+ label: 'Select this one',
+ value: 'EUParent',
+ shortcut: '',
+ },
+ {
+ label: 'Do not select',
+ value: 'other',
+ shortcut: '',
+ },
+ ],
+ key: 'euParentInstitution',
+ conditional: {
+ show: true,
+ conjunction: 'all',
+ conditions: [
+ {
+ component: 'rwaImpact',
+ operator: 'isEqual',
+ value: 'yes',
+ },
+ ],
+ },
+ type: 'radio',
+ input: true,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ label: 'tab3',
+ key: 'marketRiskTab',
+ components: [
+ {
+ label: 'mr',
+ tableView: true,
+ key: 'mr',
+ conditional: {
+ show: true,
+ conjunction: 'all',
+ conditions: [
+ {
+ component: 'affectedRiskTypes',
+ operator: 'isEqual',
+ value: 'marketRisk',
+ },
+ ],
+ },
+ type: 'container',
+ input: true,
+ components: [
+ {
+ label: 'Quantitative Information',
+ tableView: false,
+ key: 'quantitativeInformation',
+ type: 'container',
+ input: true,
+ components: [
+ {
+ title: 'Fill this tab after tab1',
+ theme: 'primary',
+ customClass: 'tooltip-text-left',
+ collapsible: false,
+ key: 'quantitativeInformation',
+ type: 'panel',
+ label: '3.2 Quantitative information',
+ input: false,
+ tableView: false,
+ components: [
+ {
+ label: 'Here select yes',
+ labelPosition: 'left-left',
+ optionsLabelPosition: 'right',
+ inline: true,
+ tableView: false,
+ values: [
+ {
+ label: 'Yes',
+ value: 'yes',
+ shortcut: '',
+ },
+ {
+ label: 'No',
+ value: 'no',
+ shortcut: '',
+ },
+ ],
+ key: 'cva',
+ customConditional:
+ "show = _.get(data, 'affectedRiskTypes.creditValuationRiskAdjustment') === false && data.rwaImpact === 'yes';",
+ type: 'radio',
+ labelWidth: 100,
+ input: true,
+ },
+ {
+ label: 'Do not select',
+ tableView: false,
+ defaultValue: false,
+ key: 'sameRiskCategories',
+ conditional: {
+ show: true,
+ conjunction: 'all',
+ conditions: [
+ {
+ component: 'rwaImpact',
+ operator: 'isEqual',
+ value: 'yes',
+ },
+ ],
+ },
+ type: 'checkbox',
+ input: true,
+ },
+ {
+ label: 'Do not select',
+ tableView: true,
+ key: 'sameImpactAcrossEntities',
+ conditional: {
+ show: true,
+ conjunction: 'all',
+ conditions: [
+ {
+ component: 'euParentInstitution',
+ operator: 'isEqual',
+ value: 'EUParent',
+ },
+ ],
+ },
+ type: 'checkbox',
+ optionsLabelPosition: 'right',
+ input: true,
+ defaultValue: false,
+ },
+ {
+ label:
+ 'Try to add a row in this grid, it will disappear',
+ tableView: true,
+ templates: {
+ header:
+ ' \n Legal entities \n Level of consolidation \n Max change of risk number \n \n ',
+ row: " \r\n \r\n {{ _.get(row, 'legalEntity.longName', '') }}\r\n \r\n \r\n {{ _.get(row, 'consolidationLevel', '') }}\r\n \r\n \r\n \r\n\t{% \r\n\tvar items = [\r\n _.get(row, 'VaRRelChange1Day', ''),\r\n _.get(row, 'VaRRelChange', ''),\r\n _.get(row, 'sVarRelChange1Day', ''),\r\n _.get(row, 'sVarRelChange', ''),\r\n _.get(row, 'IRCRelChange1Day', ''),\r\n _.get(row, 'IRCRelChange', ''),\r\n _.get(row, 'CRMRelChange1Day', ''),\r\n _.get(row, 'CRMRelChange', ''),\r\n ].filter((i) => typeof i !== 'undefined' && !isNaN(i))\r\n .map((i) => Math.abs(i));\r\n\tif (items.length) { %}\r\n {{Math.max(\r\n ...items).toFixed(3)}}%\r\n {% } else { %}\r\n {{0.000}}%\r\n {% } %}\r\n \r\n \r\n {% if (instance.options.readOnly) { %}\r\n \r\n {% } else { %}\r\n \r\n \r\n \r\n \r\n {% } %}\r\n \r\n ",
+ },
+ addAnother: 'Add legal entity',
+ modal: true,
+ saveRow: 'Close',
+ rowDrafts: false,
+ key: 'impactsPerEntity',
+ conditional: {
+ show: true,
+ conjunction: 'all',
+ conditions: [
+ {
+ component: 'rwaImpact',
+ operator: 'isEqual',
+ value: 'yes',
+ },
+ ],
+ },
+ type: 'editgrid',
+ displayAsTable: false,
+ alwaysEnabled: true,
+ input: true,
+ components: [
+ {
+ title:
+ 'Try to add a row in this grid, it will disappear',
+ theme: 'primary',
+ collapsible: false,
+ key: 'entitiesPanel',
+ type: 'panel',
+ label: 'Panel',
+ input: false,
+ tableView: false,
+ components: [
+ {
+ title:
+ 'Try to add a row in this grid, it will disappear',
+ collapsible: false,
+ key: 'periodImpactEstimationPanel',
+ customConditional:
+ "show = (\n (_.get(data, 'mr.quantitativeInformation.sameImpactAcrossEntities') === false) ||\n (_.get(data, 'euParentInstitution') === 'other')\n)",
+ type: 'panel',
+ label: 'Panel',
+ input: false,
+ tableView: false,
+ components: [
+ {
+ label: 'Number',
+ applyMaskOn: 'change',
+ mask: false,
+ tableView: true,
+ delimiter: false,
+ requireDecimal: false,
+ inputFormat: 'plain',
+ truncateMultipleSpaces: false,
+ key: 'number',
+ type: 'number',
+ input: true,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ path: 'mrEditGrid',
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ key: 'nmcTab',
+ type: 'tabs',
+ tableView: false,
+ input: false,
+ keyModified: true,
+ },
+ {
+ label: 'Submit',
+ action: 'saveState',
+ showValidations: false,
+ tableView: false,
+ key: 'submit',
+ type: 'button',
+ input: true,
+ alwaysEnabled: false,
+ state: 'draft',
+ },
+ ],
+};
+
+const form4 = {
+ title: 'form4',
+ name: 'form4',
+ path: 'form4',
+ type: 'form',
+ display: 'form',
+ components: [
+ {
+ label: 'Text Field',
+ applyMaskOn: 'change',
+ tableView: true,
+ key: 'textField',
+ type: 'textfield',
+ input: true,
+ },
+ {
+ label: 'Edit Grid',
+ tableView: false,
+ calculateValue:
+ 'if (options.server){\r\nvalue = [{ "textArea": "test"}];\r\n}',
+ calculateServer: true,
+ rowDrafts: false,
+ key: 'editGrid',
+ type: 'editgrid',
+ displayAsTable: false,
+ input: true,
+ components: [
+ {
+ label: 'Text Area',
+ applyMaskOn: 'change',
+ autoExpand: false,
+ tableView: true,
+ key: 'textArea',
+ type: 'textarea',
+ input: true,
+ },
+ ],
+ },
+ {
+ type: 'button',
+ label: 'Submit',
+ key: 'submit',
+ disableOnInvalid: true,
+ input: true,
+ tableView: false,
+ },
+ ],
+};
+
+const form5 = {
+ title: 'form5',
+ name: 'form5',
+ path: 'form5',
+ type: 'form',
+ display: 'form',
+ components: [
+ {
+ label: 'generalInformation',
+ tableView: false,
+ key: 'generalInformation',
+ type: 'container',
+ input: true,
+ components: [
+ {
+ title: 'Select here one entry',
+ theme: 'primary',
+ collapsible: false,
+ key: 'sectionOutsourcingSupervisedEntites',
+ type: 'panel',
+ label: 'Outsourcing supervised entities',
+ tableView: false,
+ input: false,
+ components: [
+ {
+ label: 'Select the only option here',
+ widget: 'choicesjs',
+ description: ' ',
+ tableView: true,
+ multiple: true,
+ dataSrc: 'json',
+ data: {
+ json: [
+ {
+ id: 6256,
+ longName: 'Bank_DE',
+ leiCode: 'LEI6256',
+ countryCode: 'DE',
+ },
+ ],
+ },
+ template:
+ ' {{ item.longName }} [{{item.countryCode}}] {{item.leiCode}}',
+ customOptions: {
+ searchResultLimit: 100,
+ fuseOptions: {
+ threshold: 0.1,
+ distance: 9000,
+ },
+ },
+ validate: {
+ required: true,
+ },
+ key: 'listSupervisedEntitiesCovered',
+ type: 'select',
+ input: true,
+ searchThreshold: 0.3,
+ },
+ ],
+ path: 'section12OutsourcingSupervisedEntities',
+ },
+ {
+ title: '1.5 Country-specific questions',
+ collapsible: false,
+ key: 'countrySpecificQuestionsPanel',
+ customConditional:
+ 'var listSupervisedEntitiesCovered = _.get(data, "generalInformation.listSupervisedEntitiesCovered", []);\r\nshow = listSupervisedEntitiesCovered.some(entity => (entity.countryCode == "LU") || (entity.countryCode == "DE"));',
+ type: 'panel',
+ label: 'Panel',
+ input: false,
+ tableView: false,
+ components: [
+ {
+ label: 'DE specific questions',
+ tableView: false,
+ key: 'deSpecific',
+ customConditional:
+ 'var listSupervisedEntitiesCovered = _.get(data, "generalInformation.listSupervisedEntitiesCovered", []);\r\nshow = listSupervisedEntitiesCovered.some(entity => entity.countryCode == "DE");',
+ type: 'container',
+ input: true,
+ components: [
+ {
+ title: 'Additional questions for DE entities',
+ collapsible: false,
+ key: 'panel',
+ type: 'panel',
+ label: 'Panel',
+ input: false,
+ tableView: false,
+ components: [
+ {
+ title: 'Sub-outsourcing',
+ collapsible: false,
+ key: 'suboutsourcing',
+ type: 'panel',
+ label: 'Panel',
+ input: false,
+ tableView: false,
+ components: [
+ {
+ label: 'Here select yes',
+ optionsLabelPosition: 'right',
+ customClass: 'tooltip-text-left',
+ inline: true,
+ tableView: false,
+ values: [
+ {
+ label: 'Yes',
+ value: 'yes',
+ shortcut: '',
+ },
+ {
+ label: 'No',
+ value: 'no',
+ shortcut: '',
+ },
+ ],
+ validate: {
+ required: true,
+ },
+ key: 'criticalPartsToBeOutsourcedSuboutsourcer',
+ type: 'radio',
+ labelWidth: 100,
+ input: true,
+ },
+ {
+ label: 'add a row, it will be removed upon save',
+ customClass: 'ml-3',
+ tableView: true,
+ templates: {
+ header:
+ ' \r\n Name \r\n Location \r\n Location of the data \r\n \r\n ',
+ row: ' \r\n \r\n {{ _.get(row, \'nameSuboutsourcer\', \'\') }}\r\n \r\n \r\n {{ _.get(row, \'locationSuboutsourcer.name\', \'\') }}\r\n \r\n \r\n {{ _.get(row, \'locationDataSub.name\', \'\') }}\r\n \r\n \r\n {% if (instance.options.readOnly) { %}\r\n \r\n {% } else { %}\r\n \r\n \r\n \r\n \r\n {% } %}\r\n \r\n ',
+ },
+ addAnother: 'Add suboutsourcer',
+ modal: true,
+ saveRow: 'Close',
+ validate: {
+ required: true,
+ },
+ rowDrafts: false,
+ key: 'suboutsourcers',
+ conditional: {
+ show: true,
+ conjunction: 'all',
+ conditions: [
+ {
+ component:
+ 'generalInformation.deSpecific.criticalPartsToBeOutsourcedSuboutsourcer',
+ operator: 'isEqual',
+ value: 'yes',
+ },
+ ],
+ },
+ type: 'editgrid',
+ displayAsTable: false,
+ input: true,
+ components: [
+ {
+ title: 'Sub-outsourcer(s)',
+ theme: 'primary',
+ collapsible: false,
+ key: 'suboutsourcerS',
+ type: 'panel',
+ label: 'Panel',
+ input: false,
+ tableView: false,
+ components: [
+ {
+ label: 'This edit grid row will disappear',
+ applyMaskOn: 'change',
+ tableView: true,
+ validate: {
+ required: true,
+ maxLength: 100,
+ },
+ key: 'nameSuboutsourcer',
+ type: 'textfield',
+ input: true,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ label: 'Submit',
+ action: 'saveState',
+ showValidations: false,
+ tableView: false,
+ key: 'submit',
+ type: 'button',
+ input: true,
+ state: 'draft',
+ },
+ ],
+};
+
+const form6 = {
+ title: 'form6',
+ name: 'form6',
+ path: 'form6',
+ type: 'form',
+ display: 'form',
+ components: [
+ {
+ label: 'Text Field',
+ applyMaskOn: 'change',
+ tableView: true,
+ key: 'textField',
+ type: 'textfield',
+ input: true,
+ },
+ {
+ label: 'Container',
+ tableView: false,
+ key: 'container',
+ type: 'container',
+ input: true,
+ components: [
+ {
+ label: 'Edit Grid',
+ tableView: false,
+ rowDrafts: false,
+ key: 'editGrid',
+ logic: [
+ {
+ name: 'ret',
+ trigger: {
+ type: 'simple',
+ simple: {
+ show: true,
+ conjunction: 'all',
+ conditions: [
+ {
+ component: 'textField',
+ operator: 'isEqual',
+ value: 'show',
+ },
+ ],
+ },
+ },
+ actions: [
+ {
+ name: 'ter',
+ type: 'value',
+ value:
+ "value = [{number:1, textArea: 'test'}, {number:2, textArea: 'test2'}]",
+ },
+ ],
+ },
+ ],
+ type: 'editgrid',
+ displayAsTable: false,
+ input: true,
+ components: [
+ {
+ label: 'Number',
+ applyMaskOn: 'change',
+ mask: false,
+ tableView: false,
+ delimiter: false,
+ requireDecimal: false,
+ inputFormat: 'plain',
+ truncateMultipleSpaces: false,
+ key: 'number',
+ type: 'number',
+ input: true,
+ },
+ {
+ label: 'Text Area',
+ applyMaskOn: 'change',
+ autoExpand: false,
+ tableView: true,
+ key: 'textArea',
+ type: 'textarea',
+ input: true,
+ },
+ ],
+ },
+ ],
+ },
+ {
+ type: 'button',
+ label: 'Submit',
+ key: 'submit',
+ disableOnInvalid: true,
+ input: true,
+ tableView: false,
+ },
+ ],
+};
+
+export default { form1, form2, form3, form4, form5, form6 };
diff --git a/src/components/email/Email.js b/src/components/email/Email.js
index 6104d63521..fd467e86f0 100644
--- a/src/components/email/Email.js
+++ b/src/components/email/Email.js
@@ -26,7 +26,6 @@ export default class EmailComponent extends TextFieldComponent {
init() {
super.init();
- this.validators.push('email');
}
get defaultSchema() {
diff --git a/src/components/email/Email.unit.js b/src/components/email/Email.unit.js
index 21ba4bed10..0769cf3d4c 100644
--- a/src/components/email/Email.unit.js
+++ b/src/components/email/Email.unit.js
@@ -51,11 +51,11 @@ describe('Email Component', () => {
setTimeout(() => {
if (valid) {
- assert.equal(!!component.error, false, 'Should not contain error');
+ assert.equal(component.errors.length, 0, 'Should not contain error');
}
else {
- assert.equal(!!component.error, true, 'Should contain error');
- assert.equal(component.error.message, error, 'Should contain error message');
+ assert(component.errors.length > 0, 'Should contain error');
+ assert.equal(component.errors[0].message, error, 'Should contain error message');
assert.equal(component.element.classList.contains('has-error'), true, 'Should contain error class');
assert.equal(component.refs.messageContainer.textContent.trim(), error, 'Should show error');
}
@@ -108,11 +108,11 @@ describe('Email Component', () => {
setTimeout(() => {
if (valid) {
- assert.equal(!!component.error, false, 'Should not contain error');
+ assert.equal(component.errors.length, 0, 'Should not contain error');
}
else {
- assert.equal(!!component.error, true, 'Should contain error');
- assert.equal(component.error.message.trim(), error, 'Should contain error message');
+ assert(component.errors.length > 0, 'Should contain error');
+ assert.equal(component.errors[0].message.trim(), error, 'Should contain error message');
assert.equal(component.element.classList.contains('has-error'), true, 'Should contain error class');
assert.equal(component.refs.messageContainer.textContent.trim(), error, 'Should show error');
}
@@ -189,11 +189,11 @@ describe('Email Component', () => {
setTimeout(() => {
if (valid) {
- assert.equal(!!component.error, false, 'Should not contain error');
+ assert.equal(component.errors.length, 0, 'Should not contain error');
}
else {
- assert.equal(!!component.error, true, 'Should contain error');
- assert.equal(component.error.message.trim(), error, 'Should contain error message');
+ assert(component.errors.length > 0, 'Should contain error');
+ assert.equal(component.errors[0].message.trim(), error, 'Should contain error message');
assert.equal(component.element.classList.contains('has-error'), true, 'Should contain error class');
assert.equal(component.refs.messageContainer.textContent.trim(), error, 'Should show error');
}
diff --git a/src/components/form/Form.js b/src/components/form/Form.js
index 957c802be3..9d2eea961b 100644
--- a/src/components/form/Form.js
+++ b/src/components/form/Form.js
@@ -1,3 +1,4 @@
+/* eslint-disable max-statements */
import _ from 'lodash';
import Component from '../_classes/component/Component';
import ComponentModal from '../_classes/componentModal/ComponentModal';
@@ -164,8 +165,8 @@ export default class FormComponent extends Component {
}
}
+ /* eslint-disable max-statements */
getSubOptions(options = {}) {
- options.parentPath = `${this.path}.data.`;
options.events = this.createEmitter();
// Make sure to not show the submit button in wizards in the nested forms.
@@ -219,8 +220,19 @@ export default class FormComponent extends Component {
if (this.options.preview) {
options.preview = this.options.preview;
}
+ if (this.options.inEditGrid) {
+ options.inEditGrid = this.options.inEditGrid;
+ }
+ if (this.options.saveDraft) {
+ options.saveDraft = this.options.saveDraft;
+ options.formio = new Formio(this.formSrc);
+ }
+ if (this.options.saveDraftThrottle) {
+ options.saveDraftThrottle = this.options.saveDraftThrottle;
+ }
return options;
}
+ /* eslint-enable max-statements */
render() {
if (this.builderMode) {
@@ -487,7 +499,13 @@ export default class FormComponent extends Component {
}
else if (this.formSrc) {
this.subFormLoading = true;
- return (new Formio(this.formSrc)).loadForm({ params: { live: 1 } })
+ const options = this.root.formio?.base && this.root.formio?.projectUrl
+ ? {
+ base: this.root.formio.base,
+ project: this.root.formio.projectUrl,
+ }
+ : {};
+ return (new Formio(this.formSrc, options)).loadForm({ params: { live: 1 } })
.then((formObj) => {
this.formObj = formObj;
if (this.options.pdf && this.component.useOriginalRevision) {
@@ -508,15 +526,15 @@ export default class FormComponent extends Component {
return this.dataValue?.data || {};
}
- checkComponentValidity(data, dirty, row, options) {
+ checkComponentValidity(data, dirty, row, options, errors = []) {
options = options || {};
const silentCheck = options.silentCheck || false;
if (this.subForm) {
- return this.subForm.checkValidity(this.subFormData, dirty, null, silentCheck);
+ return this.subForm.checkValidity(this.subFormData, dirty, null, silentCheck, errors);
}
- return super.checkComponentValidity(data, dirty, row, options);
+ return super.checkComponentValidity(data, dirty, row, options, errors);
}
checkComponentConditions(data, flags, row) {
@@ -585,7 +603,7 @@ export default class FormComponent extends Component {
*
* @return {*}
*/
- submitSubForm(rejectOnError) {
+ submitSubForm() {
// If we wish to submit the form on next page, then do that here.
if (this.shouldSubmit) {
return this.subFormReady.then(() => {
@@ -593,6 +611,7 @@ export default class FormComponent extends Component {
return this.dataValue;
}
this.subForm.nosubmit = false;
+ this.subForm.submitted = true;
return this.subForm.submitForm().then(result => {
this.subForm.loading = false;
this.subForm.showAllErrors = false;
@@ -600,13 +619,8 @@ export default class FormComponent extends Component {
return this.dataValue;
}).catch(err => {
this.subForm.showAllErrors = true;
- if (rejectOnError) {
- this.subForm.onSubmissionError(err);
- return Promise.reject(err);
- }
- else {
- return {};
- }
+ this.subForm.onSubmissionError(err);
+ return Promise.reject(err);
});
});
}
@@ -629,6 +643,10 @@ export default class FormComponent extends Component {
*/
beforeSubmit() {
const submission = this.dataValue;
+ // Cancel triggered saveDraft
+ if (this.subForm?.draftEnabled && this.subForm.triggerSaveDraft?.cancel) {
+ this.subForm.triggerSaveDraft.cancel();
+ }
const isAlreadySubmitted = submission && submission._id && submission.form;
@@ -688,7 +706,13 @@ export default class FormComponent extends Component {
if (shouldLoadSubmissionById) {
const formId = submission.form || this.formObj.form || this.component.form;
const submissionUrl = `${this.subForm.formio.formsUrl}/${formId}/submission/${submission._id}`;
- this.subForm.setUrl(submissionUrl, this.options);
+ const options = this.root.formio?.base && this.root.formio?.projectUrl
+ ? {
+ base: this.root.formio.base,
+ project: this.root.formio.projectUrl,
+ }
+ : {};
+ this.subForm.setUrl(submissionUrl, { ...this.options, ...options });
this.subForm.loadSubmission().catch((err) => {
console.error(`Unable to load subform submission ${submission._id}:`, err);
});
diff --git a/src/components/html/HTML.js b/src/components/html/HTML.js
index cd4838da13..08cc0cdc99 100644
--- a/src/components/html/HTML.js
+++ b/src/components/html/HTML.js
@@ -45,13 +45,15 @@ export default class HTMLComponent extends Component {
}
const submission = _.get(this.root, 'submission', {});
- const content = this.component.content ? this.interpolate(this.component.content, {
- metadata: submission.metadata || {},
- submission: submission,
- data: this.rootValue,
- row: this.data
+ const content = this.component.content ? this.interpolate(
+ this.sanitize(this.component.content, this.shouldSanitizeValue),
+ {
+ metadata: submission.metadata || {},
+ submission: submission,
+ data: this.rootValue,
+ row: this.data
}) : '';
- return this.sanitize(content, this.shouldSanitizeValue);
+ return content;
}
get singleTags() {
@@ -92,8 +94,17 @@ export default class HTMLComponent extends Component {
return super.render(this.renderContent());
}
+ get dataReady() {
+ return this.root?.submissionReady || Promise.resolve();
+ }
+
attach(element) {
this.loadRefs(element, { html: 'single' });
+ this.dataReady.then(() => {
+ if (this.element) {
+ this.setContent(this.elemet, this.content);
+ }
+ });
return super.attach(element);
}
}
diff --git a/src/components/html/HTML.unit.js b/src/components/html/HTML.unit.js
index 1ffe31ebb1..73906c2cbd 100644
--- a/src/components/html/HTML.unit.js
+++ b/src/components/html/HTML.unit.js
@@ -1,3 +1,4 @@
+import Webform from '../../Webform';
import Harness from '../../../test/harness';
import HTMLComponent from './HTML';
import sinon from 'sinon';
@@ -5,7 +6,8 @@ import assert from 'power-assert';
import {
comp1,
- comp2
+ comp2,
+ comp3,
} from './fixtures';
describe('HTML Component', () => {
@@ -30,4 +32,30 @@ describe('HTML Component', () => {
assert.equal(emit.callCount, 0);
});
});
+
+ it('Should not execute scripts inside HTML component, but execute interpolation properly', (done) => {
+ const formElement = document.createElement('div');
+ const form = new Webform(formElement);
+
+ const alert = sinon.spy(window, 'alert');
+ form.setForm(comp3).then(() => {
+ setTimeout(() => {
+ assert.equal(alert.callCount, 0);
+ const div = form.element.querySelector('.myClass');
+ assert.equal(div.innerHTML.trim(), 'No Text');
+
+ const textField = form.getComponent(['textField']);
+ textField.setValue('apple', { modified: true });
+
+ setTimeout(() => {
+ const div = form.element.querySelector('.myClass');
+
+ assert.equal(div.innerHTML.trim(), 'apple');
+ assert.equal(div.className, 'myClass apple-class');
+ done();
+ }, 400);
+ }, 200);
+ })
+ .catch(done);
+ });
});
diff --git a/src/components/html/fixtures/comp3.js b/src/components/html/fixtures/comp3.js
new file mode 100644
index 0000000000..bc9fcab428
--- /dev/null
+++ b/src/components/html/fixtures/comp3.js
@@ -0,0 +1,38 @@
+export default {
+ type: 'form',
+ display: 'form',
+ components: [
+ {
+ label: 'Text Field',
+ applyMaskOn: 'change',
+ tableView: true,
+ key: 'textField',
+ type: 'textfield',
+ input: true,
+ },
+ {
+ label: 'HTML',
+ attrs: [
+ {
+ attr: '',
+ value: '',
+ },
+ ],
+ content: ' \n {{' +
+ ' data.textField ? data.textField : \'No Text\'}} ',
+ refreshOnChange: true,
+ key: 'html',
+ type: 'htmlelement',
+ input: false,
+ tableView: false,
+ },
+ {
+ type: 'button',
+ label: 'Submit',
+ key: 'submit',
+ disableOnInvalid: true,
+ input: true,
+ tableView: false,
+ },
+ ],
+};
diff --git a/src/components/html/fixtures/index.js b/src/components/html/fixtures/index.js
index 7c38dd8065..63f18da5cf 100644
--- a/src/components/html/fixtures/index.js
+++ b/src/components/html/fixtures/index.js
@@ -1,3 +1,4 @@
import comp1 from './comp1';
import comp2 from './comp2';
-export { comp1, comp2 };
+import comp3 from './comp3';
+export { comp1, comp2, comp3 };
diff --git a/src/components/number/Number.js b/src/components/number/Number.js
index 7e8e953aa5..2b11773a61 100644
--- a/src/components/number/Number.js
+++ b/src/components/number/Number.js
@@ -51,7 +51,6 @@ export default class NumberComponent extends Input {
constructor(...args) {
super(...args);
- this.validators = this.validators.concat(['min', 'max']);
const separators = getNumberSeparators(this.options.language || navigator.language);
diff --git a/src/components/number/Number.unit.js b/src/components/number/Number.unit.js
index 72a67c31c2..79aa6c1b24 100644
--- a/src/components/number/Number.unit.js
+++ b/src/components/number/Number.unit.js
@@ -382,11 +382,11 @@ describe('Number Component', () => {
setTimeout(() => {
if (valid) {
- assert.equal(!!component.error, false, 'Should not contain error');
+ assert.equal(component.errors.length, 0, 'Should not contain error');
}
else {
- assert.equal(!!component.error, true, 'Should contain error');
- assert.equal(component.error.message, error, 'Should contain error message');
+ assert(component.errors.length > 0, 'Should contain error');
+ assert.equal(component.errors[0].message, error, 'Should contain error message');
assert.equal(component.element.classList.contains('has-error'), true, 'Should contain error class');
assert.equal(component.refs.messageContainer.textContent.trim(), error, 'Should show error');
}
diff --git a/src/components/panel/Panel.js b/src/components/panel/Panel.js
index b6a3aeb32b..264232e9cd 100644
--- a/src/components/panel/Panel.js
+++ b/src/components/panel/Panel.js
@@ -1,5 +1,5 @@
import NestedComponent from '../_classes/nested/NestedComponent';
-import { hasInvalidComponent } from '../../utils/utils';
+import { isChildOf } from '../../utils/utils';
import FormComponent from '../form/Form';
export default class PanelComponent extends NestedComponent {
@@ -45,18 +45,11 @@ export default class PanelComponent extends NestedComponent {
constructor(...args) {
super(...args);
this.noField = true;
- this.on('componentError', () => {
+ this.on('componentError', (err) => {
//change collapsed value only when the panel is collapsed to avoid additional redrawing that prevents validation messages
- if (hasInvalidComponent(this) && this.collapsed) {
+ if (isChildOf(err.instance, this) && this.collapsed) {
this.collapsed = false;
}
});
}
-
- getComponent(path, fn, originalPath) {
- if (this.root?.parent instanceof FormComponent) {
- path = path.replace(this._parentPath, '');
- }
- return super.getComponent(path, fn, originalPath);
- }
}
diff --git a/src/components/panel/Panel.unit.js b/src/components/panel/Panel.unit.js
index ba416b8115..ce6f6eb999 100644
--- a/src/components/panel/Panel.unit.js
+++ b/src/components/panel/Panel.unit.js
@@ -25,8 +25,11 @@ describe('Panel Component', () => {
const textComp = form.getComponent('textField');
assert.equal(panel.collapsed, false);
- assert.equal(!!numberComp.error, false);
- assert.equal(!!textComp.error, false);
+ assert.equal(numberComp.errors.length, 0);
+ assert.equal(textComp.errors.length, 1);
+
+ // Make sure the error is not visible in the UI.
+ assert.equal(numberComp.element.classList.contains('has-error'), false, 'Should not contain error classes.');
const numberInput = numberComp.refs?.input[0];
numberInput.value = 5;
@@ -34,10 +37,10 @@ describe('Panel Component', () => {
numberInput.dispatchEvent(inputEvent);
setTimeout(() => {
- assert.equal(!!numberComp.error, true);
- assert.equal(numberComp.error.messages.length, 1);
+ assert(numberComp.errors.length > 0);
+ assert.equal(numberComp.errors.length, 1);
assert.equal(numberComp.refs.messageContainer.querySelectorAll('.error').length, 1);
- assert.equal(!!textComp.error, false);
+ assert.equal(textComp.errors.length, 1);
const clickEvent = new Event('click');
panel.refs.header.dispatchEvent(clickEvent);
@@ -48,10 +51,10 @@ describe('Panel Component', () => {
setTimeout(() => {
assert.equal(panel.collapsed, false);
- assert.equal(!!numberComp.error, true);
- assert.equal(numberComp.error.messages.length, 1);
+ assert(numberComp.errors.length > 0);
+ assert.equal(numberComp.errors.length, 1);
assert.equal(numberComp.refs.messageContainer.querySelectorAll('.error').length, 1);
- assert.equal(!!textComp.error, false);
+ assert.equal(textComp.errors.length, 1);
done();
}, 300);
}, 300);
diff --git a/src/components/password/Password.unit.js b/src/components/password/Password.unit.js
index 50865d4f2a..7a2bf59b01 100644
--- a/src/components/password/Password.unit.js
+++ b/src/components/password/Password.unit.js
@@ -65,11 +65,11 @@ describe('Password Component', () => {
setTimeout(() => {
if (valid) {
- assert.equal(!!component.error, false, 'Should not contain error');
+ assert.equal(component.errors.length, 0, 'Should not contain error');
}
else {
- assert.equal(!!component.error, true, 'Should contain error');
- assert.equal(component.error.message, error, 'Should contain error message');
+ assert.equal(component.errors.length, 1, 'Should contain error');
+ assert.equal(component.errors[0].message, error, 'Should contain error message');
assert.equal(component.element.classList.contains('has-error'), true, 'Should contain error class');
assert.equal(component.refs.messageContainer.textContent.trim(), error, 'Should show error');
}
@@ -124,11 +124,11 @@ describe('Password Component', () => {
setTimeout(() => {
if (valid) {
- assert.equal(!!component.error, false, 'Should not contain error');
+ assert.equal(component.errors.length, 0, 'Should not contain error');
}
else {
- assert.equal(!!component.error, true, 'Should contain error');
- assert.equal(component.error.message.trim(), error, 'Should contain error message');
+ assert.equal(component.errors.length, 1, 'Should contain error');
+ assert.equal(component.errors[0].message.trim(), error, 'Should contain error message');
assert.equal(component.element.classList.contains('has-error'), true, 'Should contain error class');
assert.equal(component.refs.messageContainer.textContent.trim(), error, 'Should show error');
}
diff --git a/src/components/radio/Radio.js b/src/components/radio/Radio.js
index 5a97a65343..34228bda79 100644
--- a/src/components/radio/Radio.js
+++ b/src/components/radio/Radio.js
@@ -48,6 +48,23 @@ export default class RadioComponent extends ListComponent {
};
}
+ static get serverConditionSettings() {
+ return {
+ ...super.serverConditionSettings,
+ valueComponent(classComp) {
+ return {
+ type: 'select',
+ dataSrc: 'custom',
+ valueProperty: 'value',
+ dataType: classComp.dataType || '',
+ data: {
+ custom: `values = ${classComp && classComp.values ? JSON.stringify(classComp.values) : []}`,
+ },
+ };
+ },
+ };
+ }
+
static savedValueTypes(schema) {
const { boolean, string, number, object, array } = componentValueTypes;
const { dataType } = schema;
@@ -114,7 +131,6 @@ export default class RadioComponent extends ListComponent {
init() {
super.init();
this.templateData = {};
- this.validators = this.validators.concat(['select', 'onlyAvailableItems', 'availableValueProperty']);
// Trigger an update.//
let updateArgs = [];
@@ -176,7 +192,8 @@ export default class RadioComponent extends ListComponent {
}
if (this.isSelectURL && _.isObject(this.loadedOptions[index].value)) {
- input.checked = _.isEqual(this.loadedOptions[index].value, this.dataValue);
+ const optionValue = this.component.dataType === 'string' ? JSON.stringify(this.loadedOptions[index].value) : this.loadedOptions[index].value;
+ input.checked = _.isEqual(optionValue, this.dataValue);
}
else {
input.checked = (dataValue === input.value && (input.value || this.component.dataSrc !== 'url'));
@@ -296,7 +313,6 @@ export default class RadioComponent extends ListComponent {
Formio.makeRequest(this.options.formio, 'select', url, method, body, options)
.then((response) => {
this.loading = false;
- this.error = null;
this.setItems(response);
this.optionsLoaded = true;
this.redraw();
@@ -403,21 +419,39 @@ export default class RadioComponent extends ListComponent {
* @return {*}
*/
normalizeValue(value) {
+ const dataType = this.component.dataType || 'auto';
if (value === this.emptyValue) {
return value;
}
- const isEquivalent = _.toString(value) === Number(value).toString();
+ switch (dataType) {
+ case 'auto':
- if (!isNaN(parseFloat(value)) && isFinite(value) && isEquivalent) {
- value = +value;
- }
- if (value === 'true') {
- value = true;
- }
- if (value === 'false') {
- value = false;
- }
+ if (!isNaN(parseFloat(value)) && isFinite(value) && _.toString(value) === Number(value).toString()) {
+ value = +value;
+ }
+ if (value === 'true') {
+ value = true;
+ }
+ if (value === 'false') {
+ value = false;
+ }
+ break;
+ case 'number':
+ value = +value;
+ break;
+ case 'string':
+ if (typeof value === 'object') {
+ value = JSON.stringify(value);
+ }
+ else {
+ value = String(value);
+ }
+ break;
+ case 'boolean':
+ value = !(!value || value.toString() === 'false');
+ break;
+ }
if (this.isSelectURL && this.templateData && this.templateData[value]) {
const submission = this.root.submission;
diff --git a/src/components/radio/Radio.unit.js b/src/components/radio/Radio.unit.js
index aa6a77427f..17b4e9b570 100644
--- a/src/components/radio/Radio.unit.js
+++ b/src/components/radio/Radio.unit.js
@@ -14,7 +14,8 @@ import {
comp7,
comp8,
comp9,
- comp10
+ comp10,
+ comp11
} from './fixtures';
describe('Radio Component', () => {
@@ -114,6 +115,38 @@ describe('Radio Component', () => {
});
});
+ it('Should set the Value according to Storage Type', (done) => {
+ const form = _.cloneDeep(comp11);
+ const element = document.createElement('div');
+
+ Formio.createForm(element, form).then(form => {
+ const radioNumber = form.getComponent('radioNumber');
+ const radioString = form.getComponent('radioString');
+ const radioBoolean = form.getComponent('radioBoolean');
+ const value1 = '0';
+ const value2 = 'true';
+ radioNumber.setValue(value1);
+ radioString.setValue(value1);
+ radioBoolean.setValue(value2);
+
+ const submit = form.getComponent('submit');
+ const clickEvent = new Event('click');
+ const submitBtn = submit.refs.button;
+ submitBtn.dispatchEvent(clickEvent);
+
+ setTimeout(() => {
+ assert.equal(form.submission.data.radioNumber, 0);
+ assert.equal(typeof form.submission.data.radioNumber, 'number');
+ assert.equal(form.submission.data.radioString, '0');
+ assert.equal(typeof form.submission.data.radioString, 'string');
+ assert.equal(form.submission.data.radioBoolean, true);
+ assert.equal(typeof form.submission.data.radioBoolean, 'boolean');
+ document.innerHTML = '';
+ done();
+ }, 300);
+ }).catch(done);
+ });
+
it('Should set correct data for 0s values', (done) => {
Harness.testCreate(RadioComponent, comp10).then((component) => {
component.setValue('01');
@@ -172,7 +205,7 @@ describe('Radio Component', () => {
setTimeout(() => {
assert.equal(form.errors.length, 1);
- assert.equal(radio.error.message, 'Radio is an invalid value.');
+ assert.equal(radio.errors[0].message, 'Radio is an invalid value.');
value = 'one';
radio.setValue(value);
@@ -180,7 +213,7 @@ describe('Radio Component', () => {
assert.equal(radio.getValue(), value);
assert.equal(radio.dataValue, value);
assert.equal(form.errors.length, 0);
- assert.equal(!!radio.error, false);
+ assert.equal(!!radio.errors.length, 0);
document.innerHTML = '';
done();
@@ -223,7 +256,7 @@ describe('Radio Component', () => {
setTimeout(() => {
assert.equal(form.errors.length, 0);
- assert.equal(!!radio.error, false);
+ assert.equal(!!radio.errors.length, 0);
assert.equal(radio.getValue(), values[1]);
assert.equal(radio.dataValue, values[1]);
document.innerHTML = '';
diff --git a/src/components/radio/editForm/Radio.edit.data.js b/src/components/radio/editForm/Radio.edit.data.js
index f46f60aa22..bcf084468b 100644
--- a/src/components/radio/editForm/Radio.edit.data.js
+++ b/src/components/radio/editForm/Radio.edit.data.js
@@ -75,6 +75,26 @@ export default [
json: { '===': [{ var: 'data.dataSrc' }, 'values'] },
},
},
+ {
+ type: 'select',
+ input: true,
+ label: 'Storage Type',
+ key: 'dataType',
+ clearOnHide: true,
+ tooltip: 'The type to store the data. If you select something other than autotype, it will force it to that type.',
+ weight: 12,
+ template: ' {{ item.label }}',
+ dataSrc: 'values',
+ data: {
+ values: [
+ { label: 'Autotype', value: 'auto' },
+ { label: 'String', value: 'string' },
+ { label: 'Number', value: 'number' },
+ { label: 'Boolean', value: 'boolean' },
+ { label: 'Object', value: 'object' },
+ ],
+ },
+ },
{
key: 'template',
conditional: {
diff --git a/src/components/radio/fixtures/comp11.js b/src/components/radio/fixtures/comp11.js
new file mode 100644
index 0000000000..4a9eb48c25
--- /dev/null
+++ b/src/components/radio/fixtures/comp11.js
@@ -0,0 +1,81 @@
+export default {
+ title: 'Test',
+ name: 'test',
+ path: 'test',
+ type: 'form',
+ display: 'form',
+ components: [
+ {
+ label: 'Radio',
+ optionsLabelPosition: 'right',
+ inline: false,
+ tableView: false,
+ values: [
+ {
+ label: '0',
+ value: '0',
+ shortcut: ''
+ },
+ {
+ label: '1',
+ value: '1',
+ shortcut: ''
+ }
+ ],
+ key: 'radioNumber',
+ type: 'radio',
+ dataType: 'number',
+ input: true
+ },
+ {
+ label: 'Radio',
+ optionsLabelPosition: 'right',
+ inline: false,
+ tableView: false,
+ values: [
+ {
+ label: '0',
+ value: '0',
+ shortcut: ''
+ },
+ {
+ label: '1',
+ value: '1',
+ shortcut: ''
+ }],
+ key: 'radioString',
+ dataType: 'string',
+ type: 'radio',
+ input: true
+ },
+ {
+ label: 'Radio',
+ optionsLabelPosition: 'right',
+ inline: false,
+ tableView: false,
+ values: [
+ {
+ label: 'true',
+ value: 'true',
+ shortcut: ''
+ },
+ {
+ label: 'false',
+ value: 'false',
+ shortcut: ''
+ }],
+ key: 'radioBoolean',
+ dataType: 'boolean',
+ type: 'radio',
+ input: true
+ },
+ {
+ type: 'button',
+ label: 'Submit',
+ key: 'submit',
+ disableOnInvalid: true,
+ input: true,
+ tableView: false
+ }
+ ]
+};
diff --git a/src/components/radio/fixtures/index.js b/src/components/radio/fixtures/index.js
index 9db3870977..461e633cda 100644
--- a/src/components/radio/fixtures/index.js
+++ b/src/components/radio/fixtures/index.js
@@ -8,4 +8,5 @@ import comp7 from './comp7';
import comp8 from './comp8';
import comp9 from './comp9';
import comp10 from './comp10';
-export { comp1, comp2, comp3, comp4, comp5, comp6, comp7, comp8, comp9, comp10 };
+import comp11 from './comp11';
+export { comp1, comp2, comp3, comp4, comp5, comp6, comp7, comp8, comp9, comp10, comp11 };
diff --git a/src/components/recaptcha/ReCaptcha.js b/src/components/recaptcha/ReCaptcha.js
index 90abd3902f..668f8c3a80 100644
--- a/src/components/recaptcha/ReCaptcha.js
+++ b/src/components/recaptcha/ReCaptcha.js
@@ -63,7 +63,7 @@ export default class ReCaptchaComponent extends Component {
return true;
}
- verify(actionName) {
+ async verify(actionName) {
const siteKey = _get(this.root.form, 'settings.recaptcha.siteKey');
if (!siteKey) {
console.warn('There is no Site Key specified in settings in form JSON');
@@ -73,40 +73,34 @@ export default class ReCaptchaComponent extends Component {
const recaptchaApiScriptUrl = `https://www.google.com/recaptcha/api.js?render=${_get(this.root.form, 'settings.recaptcha.siteKey')}`;
this.recaptchaApiReady = Formio.requireLibrary('googleRecaptcha', 'grecaptcha', recaptchaApiScriptUrl, true);
}
- if (this.recaptchaApiReady) {
+ try {
+ await this.recaptchaApiReady;
this.recaptchaVerifiedPromise = new Promise((resolve, reject) => {
- this.recaptchaApiReady
- .then(() => {
- if (!this.isLoading) {
- this.isLoading= true;
- grecaptcha.ready(_debounce(() => {
- grecaptcha
- .execute(siteKey, {
- action: actionName
- })
- .then((token) => {
- return this.sendVerificationRequest(token).then(({ verificationResult, token }) => {
- this.recaptchaResult = {
- ...verificationResult,
- token,
- };
- this.updateValue(this.recaptchaResult);
- return resolve(verificationResult);
- });
- })
- .catch(() => {
- this.isLoading = false;
- });
- }, 1000));
+ if (!this.isLoading) {
+ this.isLoading= true;
+ grecaptcha.ready(_debounce(async() => {
+ try {
+ const token = await grecaptcha.execute(siteKey, { action: actionName });
+ const verificationResult = await this.sendVerificationRequest(token);
+ this.recaptchaResult = {
+ ...verificationResult,
+ token,
+ };
+ this.updateValue(this.recaptchaResult);
+ this.isLoading = false;
+ return resolve(verificationResult);
}
- })
- .catch(() => {
- return reject();
- });
- }).then(() => {
- this.isLoading = false;
+ catch (err) {
+ this.isLoading = false;
+ reject(err);
+ }
+ }, 1000));
+ }
});
}
+ catch (err) {
+ this.loading = false;
+ }
}
beforeSubmit() {
@@ -118,37 +112,32 @@ export default class ReCaptchaComponent extends Component {
}
sendVerificationRequest(token) {
- return Formio.makeStaticRequest(`${Formio.projectUrl}/recaptcha?recaptchaToken=${token}`)
- .then((verificationResult) => ({ verificationResult, token }));
+ return Formio.makeStaticRequest(`${Formio.projectUrl}/recaptcha?recaptchaToken=${token}`);
}
- checkComponentValidity(data, dirty, row, options = {}) {
+ checkComponentValidity(data, dirty, row, options = {}, errors = []) {
data = data || this.rootValue;
row = row || this.data;
const { async = false } = options;
- // Verification could be async only
+ // Verification could be async only (which for now is only the case for server-side validation)
if (!async) {
- return super.checkComponentValidity(data, dirty, row, options);
+ return super.checkComponentValidity(data, dirty, row, options, errors);
}
const componentData = row[this.component.key];
if (!componentData || !componentData.token) {
- this.setCustomValidity('ReCAPTCHA: Token is not specified in submission');
+ this.setCustomValidity(this.t('reCaptchaTokenNotSpecifiedError'));
return Promise.resolve(false);
}
if (!componentData.success) {
- this.setCustomValidity('ReCAPTCHA: Token validation error');
+ this.setCustomValidity(this.t('reCaptchaTokenValidationError'));
return Promise.resolve(false);
}
- return this.hook('validateReCaptcha', componentData.token, () => Promise.resolve(true))
- .then((success) => success)
- .catch((err) => {
- this.setCustomValidity(err.message || err);
- return false;
- });
+ // Any further validation will 100% not run on the client
+ return Promise.resolve(true);
}
normalizeValue(newValue) {
diff --git a/src/components/select/Select.js b/src/components/select/Select.js
index 4fc820f0bc..0fc6ea116e 100644
--- a/src/components/select/Select.js
+++ b/src/components/select/Select.js
@@ -112,7 +112,6 @@ export default class SelectComponent extends ListComponent {
init() {
super.init();
this.templateData = {};
- this.validators = this.validators.concat(['select', 'onlyAvailableItems']);
// Trigger an update.
let updateArgs = [];
@@ -254,6 +253,10 @@ export default class SelectComponent extends ListComponent {
return super.shouldLoad;
}
+ get selectData() {
+ return this.component.selectData || super.selectData;
+ }
+
isEntireObjectDisplay() {
return this.component.dataSrc === 'resource' && this.valueProperty === 'data';
}
@@ -661,7 +664,6 @@ export default class SelectComponent extends ListComponent {
Formio.makeRequest(this.options.formio, 'select', url, method, body, options)
.then((response) => {
this.loading = false;
- this.error = null;
this.setItems(response, !!search);
})
.catch((err) => {
diff --git a/src/components/select/Select.unit.js b/src/components/select/Select.unit.js
index 59fdbefad8..e0975133ef 100644
--- a/src/components/select/Select.unit.js
+++ b/src/components/select/Select.unit.js
@@ -44,7 +44,7 @@ describe('Select Component', () => {
assert.equal(component.dataValue.value, 'a');
assert.equal(typeof component.dataValue , 'object');
done();
- }, 300);
+ }, 200);
});
});
@@ -697,7 +697,7 @@ describe('Select Component', () => {
var searchHasBeenDebounced = false;
var originalDebounce = _.debounce;
_.debounce = (fn, timeout, opts) => {
- searchHasBeenDebounced = timeout === 700;
+ searchHasBeenDebounced = true;
return originalDebounce(fn, 0, opts);
};
@@ -718,8 +718,8 @@ describe('Select Component', () => {
assert.equal(searchHasBeenDebounced, true);
done();
- }, 50);
- }, 200);
+ }, 500);
+ }, 300);
}).catch(done);
});
@@ -743,7 +743,7 @@ describe('Select Component', () => {
setTimeout(() => {
assert.equal(form.errors.length, 1);
- assert.equal(select.error.message, 'Select is an invalid value.');
+ assert.equal(select.errors[0].message, 'Select is an invalid value.');
document.innerHTML = '';
done();
}, 400);
@@ -988,21 +988,21 @@ describe('Select Component', () => {
select.pristine = false;
setTimeout(() => {
- assert(!select.error, 'Select should be valid while changing');
+ assert(!select.visibleErrors.length, 'Select should be valid while changing');
select.focusableElement.dispatchEvent(new Event('blur'));
setTimeout(() => {
- assert(select.error, 'Should set error after Select component was blurred');
+ assert(select.visibleErrors.length, 'Should set error after Select component was blurred');
done();
- }, 500);
- }, 200);
+ }, 300);
+ }, 300);
}).catch(done);
});
it('Should escape special characters in regex search field', done => {
const form = _.cloneDeep(comp17);
const element = document.createElement('div');
-
+ Formio.setProjectUrl('https://formio.form.io');
Formio.createForm(element, form).then(form => {
const select = form.getComponent('select');
const searchField = select.element.querySelector('.choices__input.choices__input--cloned');
@@ -1212,3 +1212,4 @@ describe('Select Component with Entire Object Value Property', () => {
});
});
});
+
diff --git a/src/components/select/editForm/Select.edit.data.js b/src/components/select/editForm/Select.edit.data.js
index df6ad3e1df..e860bf6d46 100644
--- a/src/components/select/editForm/Select.edit.data.js
+++ b/src/components/select/editForm/Select.edit.data.js
@@ -1,5 +1,33 @@
+import _ from 'lodash';
import { eachComponent } from '../../../utils/utils';
+const calculateSelectData = (context) => {
+ const { instance, data } = context;
+ const rawDefaultValue = instance.downloadedResources.find(resource => _.get(resource, data.valueProperty) === instance.getValue());
+ const options = { data: {}, noeval: true };
+ instance.interpolate(data.template, {
+ item: rawDefaultValue,
+ }, options);
+ return options.data.item;
+};
+
+const setSelectData = (context) => {
+ // Wait before downloadedResources will be set
+ setTimeout(() => {
+ const { instance, data } = context;
+ const selectDataComponent = instance?.root.getComponent('selectData');
+ // nothing can set if don't have downloaded resources
+ if (!selectDataComponent || !instance.getValue() || !instance.downloadedResources?.length) {
+ return;
+ }
+ // if valueProperty is not provided, we have entire object
+ const shouldCalculateUrlData = data.dataSrc === 'url' && data.data.url && data.valueProperty;
+ const shouldCalculateResourceData = data.dataSrc === 'resource' && data.data.resource && data.valueProperty;
+ const newValue = shouldCalculateUrlData || shouldCalculateResourceData ? calculateSelectData(context) : undefined;
+ selectDataComponent.setValue(newValue);
+ }, 0);
+};
+
export default [
{
key: 'dataSrc',
@@ -625,5 +653,37 @@ export default [
key: 'useExactSearch',
label: 'Use exact search',
tooltip: 'Disables search algorithm threshold.',
- }
+ },
+ {
+ key: 'defaultValue',
+ onSetItems(component) {
+ setSelectData(component.evalContext());
+ },
+ onChange(context) {
+ if (context && context.flags && context.flags.modified) {
+ setSelectData(context);
+ }
+ },
+ },
+ {
+ key: 'selectData',
+ conditional: {
+ json: { 'and': [
+ { '!==': [{ var: 'data.valueProperty' }, null] },
+ { '!==': [{ var: 'data.valueProperty' }, ''] },
+ ] },
+ },
+ },
+ {
+ key: 'template',
+ onChange(context) {
+ if (context && context.flags && context.flags.modified) {
+ const defaultValueComponent = context.instance.root.getComponent('defaultValue');
+ if (!defaultValueComponent) {
+ return;
+ }
+ setSelectData(defaultValueComponent.evalContext());
+ }
+ },
+ },
];
diff --git a/src/components/selectboxes/SelectBoxes.js b/src/components/selectboxes/SelectBoxes.js
index 79246bbd81..cbb4583db9 100644
--- a/src/components/selectboxes/SelectBoxes.js
+++ b/src/components/selectboxes/SelectBoxes.js
@@ -50,7 +50,6 @@ export default class SelectBoxesComponent extends RadioComponent {
constructor(...args) {
super(...args);
- this.validators = this.validators.concat('minSelectedCount', 'maxSelectedCount', 'availableValueProperty');
}
init() {
@@ -241,10 +240,10 @@ export default class SelectBoxesComponent extends RadioComponent {
}
}
- checkComponentValidity(data, dirty, rowData, options) {
+ checkComponentValidity(data, dirty, rowData, options, errors = []) {
const minCount = this.component.validate.minSelectedCount;
const maxCount = this.component.validate.maxSelectedCount;
- if (!this.shouldSkipValidation(data, dirty, rowData)) {
+ if (!this.shouldSkipValidation(data, rowData, options)) {
const isValid = this.isValid(data, dirty);
if ((maxCount || minCount)) {
const count = Object.keys(this.validationValue).reduce((total, key) => {
@@ -264,24 +263,27 @@ export default class SelectBoxesComponent extends RadioComponent {
if (!isValid && maxCount && count > maxCount) {
const message = this.t(
- this.component.maxSelectedCountMessage || 'You can only select up to {{maxCount}} items.',
+ this.component.maxSelectedCountMessage || 'You may only select up to {{maxCount}} items',
{ maxCount }
);
+ this.errors.push({ message });
this.setCustomValidity(message, dirty);
return false;
}
else if (!isValid && minCount && count < minCount) {
this.setInputsDisabled(false);
const message = this.t(
- this.component.minSelectedCountMessage || 'You must select at least {{minCount}} items.',
+ this.component.minSelectedCountMessage || 'You must select at least {{minCount}} items',
{ minCount }
);
+ this.errors.push({ message });
this.setCustomValidity(message, dirty);
return false;
}
}
}
- return super.checkComponentValidity(data, dirty, rowData, options);
+
+ return super.checkComponentValidity(data, dirty, rowData, options, errors);
}
validateValueAvailability(setting, value) {
diff --git a/src/components/selectboxes/SelectBoxes.unit.js b/src/components/selectboxes/SelectBoxes.unit.js
index 4033bca1c4..1a0945f010 100644
--- a/src/components/selectboxes/SelectBoxes.unit.js
+++ b/src/components/selectboxes/SelectBoxes.unit.js
@@ -130,7 +130,7 @@ describe('SelectBoxes Component', () => {
const { messageContainer } = comp.refs;
assert.equal(
messageContainer.textContent.trim(),
- 'You must select at least 2 items.'
+ 'You must select at least 2 items'
);
}, 300);
});
@@ -222,7 +222,7 @@ describe('SelectBoxes Component', () => {
const { messageContainer } = comp.refs;
assert.equal(
messageContainer.textContent.trim(),
- 'You can only select up to 2 items.'
+ 'You may only select up to 2 items'
);
}, 300);
});
@@ -275,7 +275,7 @@ describe('SelectBoxes Component', () => {
return new Promise(resolve => {
const values = [
{ name : 'Alabama', abbreviation : 'AL' },
- { name : 'Alaska', abbreviation: { a:2, b: 'c' } },
+ { name : 'Alaska', abbreviation: { a: 2, b: 'c' } },
{ name : 'American Samoa', abbreviation: true }
];
resolve(values);
@@ -286,9 +286,9 @@ describe('SelectBoxes Component', () => {
const selectBoxes = form.getComponent('selectBoxes');
setTimeout(()=>{
- const inputs = selectBoxes.element.querySelectorAll('input');
- inputs[1].checked = true;
- inputs[2].checked = true;
+ // TODO: previously, this was programmatically assigning a boolean value to the `input.checked` property; however,
+ // this does not bubble a change event to the form, and we need to investigate why
+ selectBoxes.setValue({ 'AL': true, '[object Object]': true, 'true': true });
setTimeout(()=>{
const submit = form.getComponent('submit');
@@ -298,19 +298,19 @@ describe('SelectBoxes Component', () => {
setTimeout(()=>{
assert.equal(form.errors.length, 1);
- assert.equal(selectBoxes.error.message, 'Invalid Value Property');
+ assert.equal(selectBoxes.errors[0].message, 'Invalid Value Property');
selectBoxes.setValue({ 'AL': true });
setTimeout(()=>{
assert.equal(form.errors.length, 0);
- assert.equal(!!selectBoxes.error, false);
+ assert.equal(!!selectBoxes.errors.length, 0);
document.innerHTML = '';
Formio.makeRequest = originalMakeRequest;
done();
}, 300);
}, 300);
- }, 300);
- }, 200);
+ }, 600);
+ }, 500);
}).catch(done);
});
});
diff --git a/src/components/survey/Survey.unit.js b/src/components/survey/Survey.unit.js
index 0a356d48f1..f3409d294a 100644
--- a/src/components/survey/Survey.unit.js
+++ b/src/components/survey/Survey.unit.js
@@ -44,14 +44,4 @@ describe('Survey Component', () => {
}
});
});
-
- it('Should require all questions for required Survey', (done) => {
- Harness.testCreate(SurveyComponent, comp2).then((component) => {
- Harness.testSetGet(component, { service: 'bad' });
- component.on('componentChange', () => {
- done();
- });
- // assert(component.element)
- });
- });
});
diff --git a/src/components/tabs/Tabs.unit.js b/src/components/tabs/Tabs.unit.js
index 57033d3b70..8156334391 100644
--- a/src/components/tabs/Tabs.unit.js
+++ b/src/components/tabs/Tabs.unit.js
@@ -1,5 +1,5 @@
import assert from 'power-assert';
-import { Formio } from '../../Formio';
+import { Formio } from '../../formio.form';
import { comp1 } from './fixtures';
describe('Tabs Component', () => {
diff --git a/src/components/tags/Tags.unit.js b/src/components/tags/Tags.unit.js
index 47414c7689..650b5a9dff 100644
--- a/src/components/tags/Tags.unit.js
+++ b/src/components/tags/Tags.unit.js
@@ -65,7 +65,7 @@ describe('Tags Component', function() {
setTimeout(() => {
const modalPreview = component.element.querySelector('[ref="openModal"]');
- assert.equal(modalPreview.textContent.trim(), 'test, test1, test2', 'All tags should be rendered inside Modal Preview');
+ assert.equal(modalPreview.textContent.trim(), 'test,test1,test2', 'All tags should be rendered inside Modal Preview');
form.destroy();
done();
}, 250);
@@ -149,14 +149,14 @@ describe('Tags Component', function() {
tags.choices.input.element.focus();
setTimeout(() => {
- assert(!tags.error, 'Tags should be valid while changing');
+ assert.equal(tags.errors.length, 0, 'Tags should be valid while changing');
tags.choices.input.element.dispatchEvent(new Event('blur'));
setTimeout(() => {
- assert(tags.error, 'Should set error after Tags component was blurred');
+ assert.equal(tags.errors.length, 1, 'Should set error after Tags component was blurred');
done();
}, 500);
- }, 300);
+ }, 350);
}).catch(done);
});
});
diff --git a/src/components/textarea/TextArea.unit.js b/src/components/textarea/TextArea.unit.js
index 0280493e23..a771fde4c6 100644
--- a/src/components/textarea/TextArea.unit.js
+++ b/src/components/textarea/TextArea.unit.js
@@ -8,7 +8,7 @@ import Harness from '../../../test/harness';
import { Formio } from './../../Formio';
import { comp1, comp2, comp3, comp4 } from './fixtures';
import TextAreaComponent from './TextArea';
-import 'ace-builds';
+window.ace = require('ace-builds');
describe('TextArea Component', () => {
it('Should build a TextArea component', () => {
@@ -94,11 +94,11 @@ describe('TextArea Component', () => {
setTimeout(() => {
if (valid) {
- assert.equal(!!component.error, false, 'Should not contain error');
+ assert.equal(component.errors.length, 0, 'Should not contain error');
}
else {
- assert.equal(!!component.error, true, 'Should contain error');
- assert.equal(component.error.message, error, 'Should contain error message');
+ assert.equal(component.errors.length, 1, 'Should contain error');
+ assert.equal(component.errors[0].message, error, 'Should contain error message');
assert.equal(component.element.classList.contains('has-error'), true, 'Should contain error class');
assert.equal(component.refs.messageContainer.textContent.trim(), error, 'Should show error');
}
@@ -165,11 +165,11 @@ describe('TextArea Component', () => {
setTimeout(() => {
if (valid) {
- assert.equal(!!component.error, false, 'Should not contain error');
+ assert.equal(component.errors.length, 0, 'Should not contain error');
}
else {
- assert.equal(!!component.error, true, 'Should contain error');
- assert.equal(component.error.message, error, 'Should contain error message');
+ assert.equal(component.errors.length, 1, 'Should contain error');
+ assert.equal(component.errors[0].message, error, 'Should contain error message');
assert.equal(component.element.classList.contains('has-error'), true, 'Should contain error class');
assert.equal(component.refs.messageContainer.textContent.trim(), error, 'Should show error');
}
@@ -224,11 +224,11 @@ describe('TextArea Component', () => {
setTimeout(() => {
if (valid) {
- assert.equal(!!component.error, false, 'Should not contain error');
+ assert.equal(component.errors.length, 0, 'Should not contain error');
}
else {
- assert.equal(!!component.error, true, 'Should contain error');
- assert.equal(component.error.message.trim(), error, 'Should contain error message');
+ assert.equal(component.errors.length, 1, 'Should contain error');
+ assert.equal(component.errors[0].message.trim(), error, 'Should contain error message');
assert.equal(component.element.classList.contains('has-error'), true, 'Should contain error class');
assert.equal(component.refs.messageContainer.textContent.trim(), error, 'Should show error');
}
@@ -441,10 +441,9 @@ describe('TextArea Component', () => {
},
});
- setTimeout(() => {
- const plainTextArea = form.getComponent(['textArea']);
- const aceTextArea = form.getComponent(['textAreaAce']);
-
+ const plainTextArea = form.getComponent(['textArea']);
+ const aceTextArea = form.getComponent(['textAreaAce']);
+ aceTextArea.editorsReady[0].then(() => {
const textAreaElement = plainTextArea.element.querySelector('textarea');
console.log(aceTextArea.editors);
const aceEditor = aceTextArea.editors[0];
@@ -469,7 +468,7 @@ describe('TextArea Component', () => {
assert.equal(aceEditor.getValue(), '');
done();
}, 300);
- }, 500);
+ });
}).catch(done);
});
diff --git a/src/components/textfield/TextField.unit.js b/src/components/textfield/TextField.unit.js
index 834ddbc071..efc77d824d 100644
--- a/src/components/textfield/TextField.unit.js
+++ b/src/components/textfield/TextField.unit.js
@@ -266,40 +266,44 @@ describe('TextField Component', () => {
Formio.createForm(element, form).then(form => {
const component = form.getComponent('textField');
- let changed = component.setValue(value);
const error = 'Text Field does not match the mask.';
-
- if (value) {
- assert.equal(changed, true, 'Should set value');
- }
-
+ const textFieldInput = component.element.querySelector('.form-control');
+ textFieldInput.value = value;
+ const event = new Event('change');
+ textFieldInput.dispatchEvent(event);
setTimeout(() => {
- assert.equal(!!component.error, false, 'Should not contain error');
-
- const textFieldInput = component.element.querySelector('.form-control');
- const event = new Event('blur');
- textFieldInput.dispatchEvent(event);
+ if (value) {
+ assert.equal(component.getValue(), value, 'Should set value');
+ }
setTimeout(() => {
- assert.equal(!!component.error, true, 'Should contain error');
- assert.equal(component.error.message, error, 'Should contain error message');
- assert.equal(component.element.classList.contains('has-error'), true, 'Should contain error class');
- assert.equal(component.refs.messageContainer.textContent.trim(), error, 'Should show error');
+ assert.equal(component.errors.length, 0, 'Should not contain error');
- value = 9999;
- changed = component.setValue(value);
+ const textFieldInput = component.element.querySelector('.form-control');
+ const event = new Event('blur');
+ textFieldInput.dispatchEvent(event);
setTimeout(() => {
- assert.equal(!!component.error, true, 'Should contain error');
- assert.equal(component.error.message, error, 'Should contain error message');
+ assert.equal(component.errors.length, 1, 'Should contain error');
+ assert.equal(component.errors[0].message, error, 'Should contain error message');
assert.equal(component.element.classList.contains('has-error'), true, 'Should contain error class');
assert.equal(component.refs.messageContainer.textContent.trim(), error, 'Should show error');
- textFieldInput.dispatchEvent(event);
+ value = 9999;
+ component.setValue(value);
setTimeout(() => {
- assert.equal(!!component.error, false, 'Should not contain error');
- done();
+ assert.equal(component.errors.length, 1, 'Should contain error');
+ assert.equal(component.errors[0].message, error, 'Should contain error message');
+ assert.equal(component.element.classList.contains('has-error'), true, 'Should contain error class');
+ assert.equal(component.refs.messageContainer.textContent.trim(), error, 'Should show error');
+
+ textFieldInput.dispatchEvent(event);
+
+ setTimeout(() => {
+ assert.equal(component.errors.length, 0, 'Should not contain error');
+ done();
+ }, 300);
}, 300);
}, 300);
}, 300);
@@ -352,11 +356,11 @@ describe('TextField Component', () => {
setTimeout(() => {
if (valid) {
- assert.equal(!!component.error, false, 'Should not contain error');
+ assert.equal(component.errors.length, 0, 'Should not contain error');
}
else {
- assert.equal(!!component.error, true, 'Should contain error');
- assert.equal(component.error.message, error, 'Should contain error message');
+ assert.equal(component.errors.length, 1, 'Should contain error');
+ assert.equal(component.errors[0].message, error, 'Should contain error message');
assert.equal(component.element.classList.contains('has-error'), true, 'Should contain error class');
assert.equal(component.refs.messageContainer.textContent.trim(), error, 'Should show error');
}
@@ -409,7 +413,7 @@ describe('TextField Component', () => {
input.dispatchEvent(inputEvent);
setTimeout(() => {
- assert.equal(!!component.error, false, 'Should not contain error');
+ assert.equal(component.errors.length, 0, 'Should not contain error');
assert.equal(component.getValue(), '99/99-99.99:99,99', 'Should set and format value');
if (_.isEqual(value, lastValue)) {
@@ -460,11 +464,11 @@ describe('TextField Component', () => {
setTimeout(() => {
if (valid) {
- assert.equal(!!component.error, false, 'Should not contain error');
+ assert.equal(component.errors.length, 0, 'Should not contain error');
}
else {
- assert.equal(!!component.error, true, 'Should contain error');
- assert.equal(component.error.message, error, 'Should contain error message');
+ assert.equal(component.errors.length, 1, 'Should contain error');
+ assert.equal(component.errors[0].message, error, 'Should contain error message');
assert.equal(component.element.classList.contains('has-error'), true, 'Should contain error class');
assert.equal(component.refs.messageContainer.textContent.trim(), error, 'Should show error');
}
@@ -521,11 +525,11 @@ describe('TextField Component', () => {
setTimeout(() => {
if (valid) {
- assert.equal(!!component.error, false, 'Should not contain error');
+ assert.equal(component.errors.length, 0, 'Should not contain error');
}
else {
- assert.equal(!!component.error, true, 'Should contain error');
- assert.equal(component.error.message, error, 'Should contain error message');
+ assert.equal(component.errors.length, 1, 'Should contain error');
+ assert.equal(component.errors[0].message, error, 'Should contain error message');
assert.equal(component.element.classList.contains('has-error'), true, 'Should contain error class');
assert.equal(component.refs.messageContainer.textContent.trim(), error, 'Should show error');
}
@@ -570,7 +574,7 @@ describe('TextField Component', () => {
input.dispatchEvent(inputEvent);
setTimeout(() => {
- assert.equal(!!component.error, false, 'Should not contain error');
+ assert.equal(component.errors.length, 0, 'Should not contain error');
assert.equal(component.getValue().toLowerCase(), 's/s/s-s:s.s,ss', 'Should set and format value');
if (_.isEqual(value, lastValue)) {
@@ -623,11 +627,11 @@ describe('TextField Component', () => {
setTimeout(() => {
if (valid) {
- assert.equal(!!component.error, false, 'Should not contain error');
+ assert.equal(component.errors.length, 0, 'Should not contain error');
}
else {
- assert.equal(!!component.error, true, 'Should contain error');
- assert.equal(component.error.message, error, 'Should contain error message');
+ assert.equal(component.errors.length, 1, 'Should contain error');
+ assert.equal(component.errors[0].message, error, 'Should contain error message');
assert.equal(component.element.classList.contains('has-error'), true, 'Should contain error class');
assert.equal(component.refs.messageContainer.textContent.trim(), error, 'Should show error');
}
@@ -672,7 +676,7 @@ describe('TextField Component', () => {
input.dispatchEvent(inputEvent);
setTimeout(() => {
- assert.equal(!!component.error, false, 'Should not contain error');
+ assert.equal(component.errors.length, 0, 'Should not contain error');
assert.equal(component.getValue(), value.expected, 'Should set and format value');
if (_.isEqual(value.value, lastValue)) {
@@ -726,11 +730,11 @@ describe('TextField Component', () => {
setTimeout(() => {
if (valid) {
- assert.equal(!!component.error, false, 'Should not contain error');
+ assert.equal(component.errors.length, 0, 'Should not contain error');
}
else {
- assert.equal(!!component.error, true, 'Should contain error');
- assert.equal(component.error.message, error, 'Should contain error message');
+ assert.equal(component.errors.length, 1, 'Should contain error');
+ assert.equal(component.errors[0].message, error, 'Should contain error message');
assert.equal(component.element.classList.contains('has-error'), true, 'Should contain error class');
assert.equal(component.refs.messageContainer.textContent.trim(), error, 'Should show error');
}
@@ -772,7 +776,7 @@ describe('TextField Component', () => {
input.dispatchEvent(inputEvent);
setTimeout(() => {
- assert.equal(!!component.error, false, 'Should not contain error');
+ assert.equal(component.errors.length, 0, 'Should not contain error');
assert.equal(component.getValue(), value.expected, 'Should set and format value');
if (_.isEqual(value.value, lastValue)) {
@@ -823,11 +827,11 @@ describe('TextField Component', () => {
assert.equal(component.getValue().maskName, mask.mask, 'Should apply correct mask');
if (valid) {
- assert.equal(!!component.error, false, 'Should not contain error');
+ assert.equal(component.errors.length, 0, 'Should not contain error');
}
else {
- assert.equal(!!component.error, true, 'Should contain error');
- assert.equal(component.error.message, error, 'Should contain error message');
+ assert.equal(component.errors.length, 1, 'Should contain error');
+ assert.equal(component.errors[0].message, error, 'Should contain error message');
assert.equal(component.element.classList.contains('has-error'), true, 'Should contain error class');
assert.equal(component.refs.messageContainer.textContent.trim(), error, 'Should show error');
}
@@ -883,11 +887,11 @@ describe('TextField Component', () => {
setTimeout(() => {
if (valid) {
- assert.equal(!!component.error, false, 'Should not contain error');
+ assert.equal(component.errors.length, 0, 'Should not contain error');
}
else {
- assert.equal(!!component.error, true, 'Should contain error');
- assert.equal(component.error.message, error, 'Should contain error message');
+ assert.equal(component.errors.length, 1, 'Should contain error');
+ assert.equal(component.errors[0].message, error, 'Should contain error message');
assert.equal(component.element.classList.contains('has-error'), true, 'Should contain error class');
assert.equal(component.refs.messageContainer.textContent.trim(), error, 'Should show error');
}
@@ -927,7 +931,7 @@ describe('TextField Component', () => {
input.dispatchEvent(inputEvent);
setTimeout(() => {
- assert.equal(!!component.error, false, 'Should not contain error');
+ assert.equal(component.errors.length, 0, 'Should not contain error');
assert.equal(component.getValue(), value.expected, 'Should set and format value');
if (_.isEqual(value.value, lastValue)) {
diff --git a/src/components/time/Time.js b/src/components/time/Time.js
index 2ea0b44c34..08d8e80af5 100644
--- a/src/components/time/Time.js
+++ b/src/components/time/Time.js
@@ -38,13 +38,6 @@ export default class TimeComponent extends TextFieldComponent {
this.rawData = this.component.multiple ? [] : this.emptyValue;
}
- init() {
- super.init();
- if (this.component.inputType === 'text') {
- this.validators.push('time');
- }
- }
-
static get builderInfo() {
return {
title: 'Time',
diff --git a/src/components/time/Time.unit.js b/src/components/time/Time.unit.js
index e42bc70dbb..3b478709af 100644
--- a/src/components/time/Time.unit.js
+++ b/src/components/time/Time.unit.js
@@ -49,17 +49,11 @@ describe('Time Component', () => {
timeInput.dispatchEvent(inputEvent);
setTimeout(() => {
- timeInput.value = '12:00';
- timeInput.dispatchEvent(inputEvent);
-
+ component.setValue('12:00');
setTimeout(() => {
- component.checkData(component.data);
-
- setTimeout(() => {
- assert.equal(component.errors.length, 0);
- done();
- }, 700);
- }, 600);
+ assert.equal(component.errors.length, 0);
+ done();
+ }, 700);
}, 500);
});
});
@@ -71,7 +65,7 @@ describe('Time Component', () => {
const component = form.components[0];
Harness.setInputValue(component, 'data[time]', '89:19');
setTimeout(() => {
- assert.equal(component.error.message, 'Invalid time', 'Should have an error');
+ assert.equal(component.errors[0].message, 'Invalid time', 'Should have an error');
done();
}, 650);
}).catch(done);
diff --git a/src/components/url/Url.js b/src/components/url/Url.js
index 56a4908ed8..4a18297fa5 100644
--- a/src/components/url/Url.js
+++ b/src/components/url/Url.js
@@ -23,7 +23,6 @@ export default class UrlComponent extends TextFieldComponent {
constructor(component, options, data) {
super(component, options, data);
- this.validators.push('url');
}
get defaultSchema() {
diff --git a/src/components/url/Url.unit.js b/src/components/url/Url.unit.js
index 094c6a2400..8ea7d04034 100644
--- a/src/components/url/Url.unit.js
+++ b/src/components/url/Url.unit.js
@@ -56,11 +56,11 @@ describe('Url Component', () => {
setTimeout(() => {
if (valid) {
- assert.equal(!!component.error, false, 'Should not contain error');
+ assert.equal(component.errors.length, 0, 'Should not contain error');
}
else {
- assert.equal(!!component.error, true, 'Should contain error');
- assert.equal(component.error.message, error, 'Should contain error message');
+ assert.equal(component.errors.length, 1, 'Should contain error');
+ assert.equal(component.errors[0].message, error, 'Should contain error message');
assert.equal(component.element.classList.contains('has-error'), true, 'Should contain error class');
assert.equal(component.refs.messageContainer.textContent.trim(), error, 'Should show error');
}
@@ -112,11 +112,11 @@ describe('Url Component', () => {
setTimeout(() => {
if (valid) {
- assert.equal(!!component.error, false, 'Should not contain error');
+ assert.equal(component.errors.length, 0, 'Should not contain error');
}
else {
- assert.equal(!!component.error, true, 'Should contain error');
- assert.equal(component.error.message.trim(), error, 'Should contain error message');
+ assert.equal(component.errors.length, 1, 'Should contain error');
+ assert.equal(component.errors[0].message.trim(), error, 'Should contain error message');
assert.equal(component.element.classList.contains('has-error'), true, 'Should contain error class');
assert.equal(component.refs.messageContainer.textContent.trim(), error, 'Should show error');
}
@@ -199,11 +199,11 @@ describe('Url Component', () => {
setTimeout(() => {
if (valid) {
- assert.equal(!!component.error, false, 'Should not contain error');
+ assert.equal(component.errors.length, 0, 'Should not contain error');
}
else {
- assert.equal(!!component.error, true, 'Should contain error');
- assert.equal(component.error.message.trim(), error, 'Should contain error message');
+ assert.equal(component.errors.length, 1, 'Should contain error');
+ assert.equal(component.errors[0].message.trim(), error, 'Should contain error message');
assert.equal(component.element.classList.contains('has-error'), true, 'Should contain error class');
assert.equal(component.refs.messageContainer.textContent.trim(), error, 'Should show error');
}
diff --git a/src/formio.embed.js b/src/formio.embed.js
index 0ee7363258..c7d9ec4e7e 100644
--- a/src/formio.embed.js
+++ b/src/formio.embed.js
@@ -26,9 +26,11 @@ if (thisScript) {
config.formioPath(scriptSrc);
}
scriptSrc = scriptSrc.join('/');
+ const debug = (query.debug === 'true' || query.debug === '1');
+ const renderer = debug ? 'formio.form' : 'formio.form.min';
Formio.config = Object.assign({
- script: query.script || (`${config.updatePath ? config.updatePath() : scriptSrc}/formio.form.min.js`),
- style: query.styles || (`${config.updatePath ? config.updatePath() : scriptSrc}/formio.form.min.css`),
+ script: query.script || (`${config.updatePath ? config.updatePath() : scriptSrc}/${renderer}.js`),
+ style: query.styles || (`${config.updatePath ? config.updatePath() : scriptSrc}/${renderer}.css`),
cdn: query.cdn,
class: (query.class || 'formio-form-wrapper'),
src: query.src,
@@ -39,7 +41,7 @@ if (thisScript) {
submit: query.submit,
includeLibs: (query.libs === 'true' || query.libs === '1'),
template: query.template,
- debug: (query.debug === 'true' || query.debug === '1'),
+ debug: debug,
config: {},
redirect: (query.return || query.redirect),
embedCSS: (`${config.updatePath ? config.updatePath() : scriptSrc}/formio.embed.css`),
diff --git a/src/formio.form.js b/src/formio.form.js
index 2c44d137e3..b61b7df3bf 100644
--- a/src/formio.form.js
+++ b/src/formio.form.js
@@ -5,12 +5,6 @@ import Components from './components/Components';
import Displays from './displays/Displays';
import Templates from './templates/Templates';
import Providers from './providers';
-import Rules from './validator/Rules';
-import Conjunctions from './validator/conjunctions';
-import Operators from './validator/operators';
-import QuickRules from './validator/quickRules';
-import Transformers from './validator/transformers';
-import ValueSources from './validator/valueSources';
import Widgets from './widgets';
import Form from './Form';
import Utils from './utils';
@@ -26,20 +20,15 @@ Formio.loadModules = (path = `${Formio.getApiUrl() }/externalModules.js`, name
};
// This is needed to maintain correct imports using the "dist" file.
+Formio.isRenderer = true;
Formio.Components = Components;
Formio.Templates = Templates;
Formio.Utils = Utils;
Formio.Form = Form;
Formio.Displays = Displays;
Formio.Providers = Providers;
-Formio.Rules = Rules;
Formio.Widgets = Widgets;
Formio.Evaluator = Evaluator;
-Formio.Conjunctions = Conjunctions;
-Formio.Operators = Operators;
-Formio.QuickRules = QuickRules;
-Formio.Transformers = Transformers;
-Formio.ValueSources = ValueSources;
Formio.AllComponents = AllComponents;
Formio.Licenses = Licenses;
@@ -54,7 +43,9 @@ Formio.Components.setComponents(AllComponents);
* @returns
*/
export function registerModule(mod, defaultFn = null, options = {}) {
- // Sanity check.
+ if (typeof mod === 'function') {
+ return registerModule(mod(Formio), defaultFn, options);
+ }
if (typeof mod !== 'object') {
return;
}
@@ -91,27 +82,9 @@ export function registerModule(mod, defaultFn = null, options = {}) {
case 'displays':
Formio.Displays.addDisplays(mod.displays);
break;
- case 'rules':
- Formio.Rules.addRules(mod.rules);
- break;
case 'evaluator':
Formio.Evaluator.registerEvaluator(mod.evaluator);
break;
- case 'conjunctions':
- Formio.Conjunctions.addConjunctions(mod.conjunctions);
- break;
- case 'operators':
- Formio.Operators.addOperators(mod.operators);
- break;
- case 'quickRules':
- Formio.QuickRules.addQuickRules(mod.quickRules);
- break;
- case 'transformers':
- Formio.Transformers.addTransformers(mod.transformers);
- break;
- case 'valueSources':
- Formio.ValueSources.addValueSources(mod.valueSources);
- break;
case 'library':
options.license
? Formio.Licenses.addLicense(mod.library, options.license)
@@ -153,4 +126,4 @@ export function useModule(defaultFn = null) {
Formio.use = useModule();
// Export the components.
-export { Components, Displays, Providers, Rules, Widgets, Templates, Conjunctions, Operators, QuickRules, Transformers, ValueSources, Utils, Form, Formio, Licenses, EventEmitter };
+export { Components, Displays, Providers, Widgets, Templates, Utils, Form, Formio, Licenses, EventEmitter };
diff --git a/src/index.js b/src/index.js
index f42ff5af5c..9275765fe3 100644
--- a/src/index.js
+++ b/src/index.js
@@ -2,6 +2,7 @@ import FormBuilder from './FormBuilder';
import Builders from './builders/Builders';
import { Formio, useModule } from './formio.form';
Formio.Builders = Builders;
+Formio.isBuilder = true;
Formio.use = useModule((key, mod) => {
if (key === 'builders') {
Formio.Builders.addBuilders(mod.builders);
@@ -10,4 +11,4 @@ Formio.use = useModule((key, mod) => {
return false;
});
export * from './formio.form';
-export { FormBuilder, Builders };
+export { FormBuilder, Builders, Formio };
diff --git a/src/providers/storage/s3.js b/src/providers/storage/s3.js
index d5795b73e2..5afc8cc7da 100644
--- a/src/providers/storage/s3.js
+++ b/src/providers/storage/s3.js
@@ -1,7 +1,12 @@
import XHR from './xhr';
import { withRetries } from './util';
-const AbortController = window.AbortController || require('abortcontroller-polyfill/dist/cjs-ponyfill');
+const loadAbortControllerPolyfill = async() => {
+ if (typeof AbortController === 'undefined') {
+ await import('abortcontroller-polyfill/dist/polyfill-patch-fetch');
+ }
+};
+
function s3(formio) {
return {
async uploadFile(file, fileName, dir, progressCallback, url, options, fileKey, groupPermissions, groupId, abortCallback, multipartOptions) {
@@ -11,6 +16,7 @@ function s3(formio) {
if (response.signed) {
if (multipartOptions && Array.isArray(response.signed)) {
// patch abort callback
+ await loadAbortControllerPolyfill();
const abortController = new AbortController();
const abortSignal = abortController.signal;
if (typeof abortCallback === 'function') {
diff --git a/src/sass/formio.form.scss b/src/sass/formio.form.scss
index 6f5c232ea2..857e7925be 100644
--- a/src/sass/formio.form.scss
+++ b/src/sass/formio.form.scss
@@ -846,8 +846,8 @@ body.formio-dialog-open {
color:#EB0000;
}
-.formio-component-radio.formio-component-label-hidden.required .label-position-right.form-check-label:before,
-.formio-component-selectboxes.formio-component-label-hidden.required .label-position-right.form-check-label:before {
+.formio-component-radio.formio-component-label-hidden.required .form-check .label-position-right.form-check-label:before,
+.formio-component-selectboxes.formio-component-label-hidden.required .form-check .label-position-right.form-check-label:before {
right: 20px;
}
diff --git a/src/templates/Templates.js b/src/templates/Templates.js
index bae9fea6f2..18152f3942 100644
--- a/src/templates/Templates.js
+++ b/src/templates/Templates.js
@@ -1,51 +1,5 @@
-import _ from 'lodash';
import templates from './index';
-
-export default class Templates {
- static get templates() {
- if (!Templates._templates) {
- Templates._templates = templates;
- }
- return Templates._templates;
- }
-
- static addTemplate(name, template) {
- Templates.templates[name] = template;
- }
-
- static extendTemplate(name, template) {
- Templates.templates[name] = _.merge({}, Templates.templates[name], template);
- }
-
- static setTemplate(name, template) {
- Templates.addTemplate(name, template);
- }
-
- static set current(templates) {
- const defaultTemplates = Templates.current;
- Templates._current = _.merge({}, defaultTemplates, templates);
- }
-
- static get current() {
- if (Templates._current) {
- return Templates._current;
- }
-
- return Templates.defaultTemplates;
- }
-
- static get defaultTemplates() {
- return Templates.templates.hasOwnProperty('bootstrap') ? Templates.templates.bootstrap : {};
- }
-
- static set framework(framework) {
- if (Templates.templates.hasOwnProperty(framework)) {
- Templates._framework = framework;
- Templates._current = Templates.templates[framework];
- }
- }
-
- static get framework() {
- return Templates._framework;
- }
-}
+import { Template } from '@formio/core/experimental';
+Template.addTemplates(templates);
+Template.defaultTemplates = templates.bootstrap;
+export default Template;
diff --git a/src/translations/en.js b/src/translations/en.js
index ed7e4848a8..986556546a 100644
--- a/src/translations/en.js
+++ b/src/translations/en.js
@@ -2,6 +2,8 @@ export default {
unsavedRowsError: 'Please save all rows before proceeding.',
invalidRowsError: 'Please correct invalid rows before proceeding.',
invalidRowError: 'Invalid row. Please correct it or delete.',
+ invalidOption: '{{field}} is an invalid value.',
+ invalidDay: '{{field}} is not a valid day.',
alertMessageWithLabel: '{{label}}: {{message}}',
alertMessage: '{{message}}',
complete: 'Submission Complete',
@@ -26,11 +28,14 @@ export default {
minDate: '{{field}} should not contain date before {{- minDate}}',
maxYear: '{{field}} should not contain year greater than {{maxYear}}',
minYear: '{{field}} should not contain year less than {{minYear}}',
+ minSelectedCount: 'You must select at least {{minCount}} items',
+ maxSelectedCount: 'You may only select up to {{maxCount}} items',
invalid_email: '{{field}} must be a valid email.', // eslint-disable-line camelcase
invalid_url: '{{field}} must be a valid url.', // eslint-disable-line camelcase
invalid_regex: '{{field}} does not match the pattern {{regex}}.', // eslint-disable-line camelcase
invalid_date: '{{field}} is not a valid date.', // eslint-disable-line camelcase
invalid_day: '{{field}} is not a valid day.', // eslint-disable-line camelcase
+ invalidValueProperty: 'Invalid Value Property',
mask: '{{field}} does not match the mask.',
valueIsNotAvailable: '{{ field }} is an invalid value.',
stripe: '{{stripe}}',
@@ -57,9 +62,13 @@ export default {
saveDraftInstanceError: 'Cannot save draft because there is no formio instance.',
saveDraftAuthError: 'Cannot save draft unless a user is authenticated.',
restoreDraftInstanceError: 'Cannot restore draft because there is no formio instance.',
+ saveDraftError: 'Unable to save draft.',
+ restoreDraftError: 'Unable to restore draft.',
time: 'Invalid time',
cancelButtonAriaLabel: 'Cancel button. Click to reset the form',
previousButtonAriaLabel:'Previous button. Click to go back to the previous tab',
nextButtonAriaLabel:'Next button. Click to go to the next tab',
submitButtonAriaLabel:'Submit Form button. Click to submit the form',
+ reCaptchaTokenValidationError: 'ReCAPTCHA: Token validation error',
+ reCaptchaTokenNotSpecifiedError: 'ReCAPTCHA: Token is not specified in submission',
};
diff --git a/src/utils/conditionOperators/IsEqualTo.js b/src/utils/conditionOperators/IsEqualTo.js
index 53c94b657d..d8eece5f12 100644
--- a/src/utils/conditionOperators/IsEqualTo.js
+++ b/src/utils/conditionOperators/IsEqualTo.js
@@ -12,7 +12,7 @@ export default class IsEqualTo extends ConditionOperator {
}
execute({ value, comparedValue, instance, conditionComponentPath }) {
- if (value && comparedValue && typeof value !== typeof comparedValue && _.isString(comparedValue)) {
+ if ((value || value === false) && comparedValue && typeof value !== typeof comparedValue && _.isString(comparedValue)) {
try {
comparedValue = JSON.parse(comparedValue);
}
diff --git a/src/utils/formUtils.js b/src/utils/formUtils.js
index c6634bbece..05db606af2 100644
--- a/src/utils/formUtils.js
+++ b/src/utils/formUtils.js
@@ -12,6 +12,7 @@ import chunk from 'lodash/chunk';
import pad from 'lodash/pad';
import { compare, applyPatch } from 'fast-json-patch';
import _ from 'lodash';
+import { fastCloneDeep } from './utils';
/**
* Determine if a component is a layout component or not.
@@ -392,7 +393,7 @@ export function applyFormChanges(form, changes) {
* @returns {Object}
* The flattened components map.
*/
-export function flattenComponents(components, includeAll) {
+export function flattenComponents(components, includeAll = false) {
const flattened = {};
eachComponent(components, (component, path) => {
flattened[path] = component;
diff --git a/src/utils/index.js b/src/utils/index.js
index 0970a5fea7..4bf07aee9a 100644
--- a/src/utils/index.js
+++ b/src/utils/index.js
@@ -2,4 +2,5 @@ import * as FormioUtils from './utils';
if (typeof global === 'object') {
global.FormioUtils = FormioUtils;
}
+export { FormioUtils as Utils };
export default FormioUtils;
diff --git a/src/utils/utils.js b/src/utils/utils.js
index 3ae7c973a4..c06750eb3f 100644
--- a/src/utils/utils.js
+++ b/src/utils/utils.js
@@ -1330,7 +1330,8 @@ export function sanitize(string, options) {
}
// Allowd URI Regex
if (options.sanitizeConfig && options.sanitizeConfig.allowedUriRegex) {
- sanitizeOptions.ALLOWED_URI_REGEXP = options.sanitizeConfig.allowedUriRegex;
+ const allowedUriRegex = options.sanitizeConfig.allowedUriRegex;
+ sanitizeOptions.ALLOWED_URI_REGEXP = _.isString(allowedUriRegex) ? new RegExp(allowedUriRegex) : allowedUriRegex;
}
// Allow to extend the existing array of elements that are safe for URI-like values
if (options.sanitizeConfig && Array.isArray(options.sanitizeConfig.addUriSafeAttr) && options.sanitizeConfig.addUriSafeAttr.length > 0) {
@@ -1382,13 +1383,14 @@ export function getArrayFromComponentPath(pathStr) {
.map(part => _.defaultTo(_.toNumber(part), part));
}
-export function hasInvalidComponent(component) {
- return component.getComponents().some((comp) => {
- if (_.isArray(comp.components)) {
- return hasInvalidComponent(comp);
+export function isChildOf(child, parent) {
+ while (child && child.parent) {
+ if (child.parent === parent) {
+ return true;
}
- return comp.error;
- });
+ child = child.parent;
+ }
+ return false;
}
export function getStringFromComponentPath(path) {
@@ -1582,6 +1584,21 @@ export function getComponentSavedTypes(fullSchema) {
return null;
}
+/**
+ * Interpolates @formio/core errors so that they are compatible with the renderer
+ * @param {FieldError[]} errors
+ * @param firstPass
+ * @returns {[]}
+ */
+export const interpolateErrors = (component, errors, interpolateFn) => {
+ return errors.map((error) => {
+ error.component = component;
+ const { errorKeyOrMessage, context } = error;
+ const toInterpolate = component.errors && component.errors[errorKeyOrMessage] ? component.errors[errorKeyOrMessage] : errorKeyOrMessage;
+ return { ...error, message: unescapeHTML(interpolateFn(toInterpolate, context)), context: { ...context } };
+ });
+};
+
export function getItemTemplateKeys(template) {
const templateKeys = [];
if (!template) {
diff --git a/src/validator/Rules.js b/src/validator/Rules.js
deleted file mode 100644
index 2fd96cc4e1..0000000000
--- a/src/validator/Rules.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import rules from './rules/index';
-
-export default class Rules {
- static rules = rules;
-
- static addRule(name, rule) {
- Rules.rules[name] = rule;
- }
-
- static addRules(rules) {
- Rules.rules = { ...Rules.rules, ...rules };
- }
-
- static getRule(name) {
- return Rules.rules[name];
- }
-
- static getRules() {
- return Rules.rules;
- }
-}
diff --git a/src/validator/Validator.js b/src/validator/Validator.js
deleted file mode 100644
index cfca1d9fab..0000000000
--- a/src/validator/Validator.js
+++ /dev/null
@@ -1,1240 +0,0 @@
-import _ from 'lodash';
-import {
- boolValue,
- getInputMask,
- matchInputMask,
- getDateSetting,
- escapeRegExCharacters,
- interpolate,
- convertFormatToMoment, getArrayFromComponentPath, unescapeHTML
-} from '../utils/utils';
-import moment from 'moment';
-import Inputmask from 'inputmask';
-import fetchPonyfill from 'fetch-ponyfill';
-const { fetch, Headers, Request } = fetchPonyfill({
- Promise: Promise
-});
-import {
- checkInvalidDate,
- CALENDAR_ERROR_MESSAGES
-} from '../utils/calendarUtils';
-import Rules from './Rules';
-
-class ValidationChecker {
- constructor(config = {}) {
- this.config = _.defaults(config, ValidationChecker.config);
- this.validators = {
- required: {
- key: 'validate.required',
- method: 'validateRequired',
- hasLabel: true,
- message(component) {
- return component.t(component.errorMessage('required'), {
- field: component.errorLabel,
- data: component.data
- });
- },
- check(component, setting, value) {
- if (!boolValue(setting) || component.isValueHidden()) {
- return true;
- }
-
- const isCalendar = component.validators.some(validator => validator === 'calendar');
-
- if (!value && isCalendar && component.widget.enteredDate) {
- return !this.validators.calendar.check.call(this, component, setting, value);
- }
-
- return !component.isEmpty(value);
- }
- },
- onlyAvailableItems: {
- key: 'validate.onlyAvailableItems',
- method: 'validateValueAvailability',
- hasLabel: true,
- message(component) {
- return component.t(component.errorMessage('valueIsNotAvailable'), {
- field: component.errorLabel,
- data: component.data
- });
- },
- check(component, setting) {
- return !boolValue(setting);
- }
- },
- unique: {
- key: 'validate.unique',
- hasLabel: true,
- message(component) {
- return component.t(component.errorMessage('unique'), {
- field: component.errorLabel,
- data: component.data
- });
- },
- check(component, setting, value) {
- // Skip if setting is falsy
- if (!boolValue(setting)) {
- return true;
- }
-
- // Skip if value is empty object or falsy
- if (!value || _.isObjectLike(value) && _.isEmpty(value)) {
- return true;
- }
-
- // Skip if we don't have a database connection
- if (!this.config.db) {
- return true;
- }
-
- return new Promise(resolve => {
- const form = this.config.form;
- const submission = this.config.submission;
- const path = `data.${component.path}`;
-
- const addPathQueryParams = (pathQueryParams, query, path) => {
- const pathArray = path.split(/\[\d+\]?./);
- const needValuesInArray = pathArray.length > 1;
-
- let pathToValue = path;
-
- if (needValuesInArray) {
- pathToValue = pathArray.shift();
- const pathQueryObj = {};
-
- _.reduce(pathArray, (pathQueryPath, pathPart, index) => {
- const isLastPathPart = index === (pathArray.length - 1);
- const obj = _.get(pathQueryObj, pathQueryPath, pathQueryObj);
- const addedPath = `$elemMatch['${pathPart}']`;
-
- _.set(obj, addedPath, isLastPathPart ? pathQueryParams : {});
-
- return pathQueryPath ? `${pathQueryPath}.${addedPath}` : addedPath;
- }, '');
-
- query[pathToValue] = pathQueryObj;
- }
- else {
- query[pathToValue] = pathQueryParams;
- }
- };
-
- // Build the query
- const query = { form: form._id };
- let collationOptions = {};
-
- if (_.isString(value)) {
- if (component.component.dbIndex) {
- addPathQueryParams(value, query, path);
- }
- // These are kind of hacky but provides for a more efficient "unique" validation when the string is an email,
- // because we (by and large) only have to worry about ASCII and partial unicode; this way, we can use collation-
- // aware indexes with case insensitive email searches to make things like login and registration a whole lot faster
- else if (
- component.component.type === 'email' ||
- (component.component.type === 'textfield' && component.component.validate?.pattern === '[A-Za-z0-9]+')
- ) {
- addPathQueryParams(value, query, path);
- collationOptions = { collation: { locale: 'en', strength: 2 } };
- }
- else {
- addPathQueryParams({
- $regex: new RegExp(`^${escapeRegExCharacters(value)}$`),
- $options: 'i'
- }, query, path);
- }
- }
- // FOR-213 - Pluck the unique location id
- else if (
- _.isPlainObject(value) &&
- value.address &&
- value.address['address_components'] &&
- value.address['place_id']
- ) {
- addPathQueryParams({
- $regex: new RegExp(`^${escapeRegExCharacters(value.address['place_id'])}$`),
- $options: 'i'
- }, query, `${path}.address.place_id`);
- }
- // Compare the contents of arrays vs the order.
- else if (_.isArray(value)) {
- addPathQueryParams({ $all: value }, query, path);
- }
- else if (_.isObject(value) || _.isNumber(value)) {
- addPathQueryParams({ $eq: value }, query, path);
- }
- // Only search for non-deleted items
- query.deleted = { $eq: null };
- query.state = 'submitted';
- const uniqueValidationCallback = (err, result) => {
- if (err) {
- return resolve(false);
- }
- else if (result) {
- // Only OK if it matches the current submission
- if (submission._id && (result._id.toString() === submission._id)) {
- resolve(true);
- }
- else {
- component.conflictId = result._id.toString();
- return resolve(false);
- }
- }
- else {
- return resolve(true);
- }
- };
- // Try to find an existing value within the form
- this.config.db.findOne(query, null, collationOptions, (err, result) => {
- if (err && collationOptions.collation) {
- // presume this error comes from db compatibility, try again as regex
- delete query[path];
- addPathQueryParams({
- $regex: new RegExp(`^${escapeRegExCharacters(value)}$`),
- $options: 'i'
- }, query, path);
- this.config.db.findOne(query, uniqueValidationCallback);
- }
- else {
- return uniqueValidationCallback(err, result);
- }
- });
- }).catch(() => false);
- }
- },
- multiple: {
- key: 'validate.multiple',
- hasLabel: true,
- message(component) {
- const shouldBeArray = boolValue(component.component.multiple) || Array.isArray(component.emptyValue);
- const isRequired = component.component.validate.required;
- const messageKey = shouldBeArray ? (isRequired ? 'array_nonempty' : 'array') : 'nonarray';
-
- return component.t(component.errorMessage(messageKey), {
- field: component.errorLabel,
- data: component.data
- });
- },
- check(component, setting, value) {
- // Skip multiple validation if the component tells us to
- if (!component.validateMultiple()) {
- return true;
- }
-
- const shouldBeArray = boolValue(setting);
- const canBeArray = Array.isArray(component.emptyValue);
- const isArray = Array.isArray(value);
- const isRequired = component.component.validate.required;
-
- if (shouldBeArray) {
- if (isArray) {
- return isRequired ? !!value.length : true;
- }
- else {
- // Null/undefined is ok if this value isn't required; anything else should fail
- return _.isNil(value) ? !isRequired : false;
- }
- }
- else {
- return canBeArray || !isArray;
- }
- }
- },
- select: {
- key: 'validate.select',
- hasLabel: true,
- message(component) {
- return component.t(component.errorMessage('select'), {
- field: component.errorLabel,
- data: component.data
- });
- },
- check(component, setting, value, data, index, row, async) {
- // Skip if setting is falsy
- if (!boolValue(setting)) {
- return true;
- }
-
- // Skip if value is empty
- if (!value || _.isEmpty(value)) {
- return true;
- }
-
- // Skip if we're not async-capable
- if (!async) {
- return true;
- }
-
- const schema = component.component;
-
- // Initialize the request options
- const requestOptions = {
- url: setting,
- method: 'GET',
- qs: {},
- json: true,
- headers: {}
- };
-
- // If the url is a boolean value
- if (_.isBoolean(requestOptions.url)) {
- requestOptions.url = !!requestOptions.url;
-
- if (
- !requestOptions.url ||
- schema.dataSrc !== 'url' ||
- !schema.data.url ||
- !schema.searchField
- ) {
- return true;
- }
-
- // Get the validation url
- requestOptions.url = schema.data.url;
-
- // Add the search field
- requestOptions.qs[schema.searchField] = value;
-
- // Add the filters
- if (schema.filter) {
- requestOptions.url += (!requestOptions.url.includes('?') ? '?' : '&') + schema.filter;
- }
-
- // If they only wish to return certain fields.
- if (schema.selectFields) {
- requestOptions.qs.select = schema.selectFields;
- }
- }
-
- if (!requestOptions.url) {
- return true;
- }
-
- // Make sure to interpolate.
- requestOptions.url = interpolate(requestOptions.url, { data: component.data });
-
- // Add query string to URL
- requestOptions.url += (requestOptions.url.includes('?') ? '&' : '?') + _.chain(requestOptions.qs)
- .map((val, key) => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`)
- .join('&')
- .value();
-
- // Set custom headers.
- if (schema.data && schema.data.headers) {
- _.each(schema.data.headers, header => {
- if (header.key) {
- requestOptions.headers[header.key] = header.value;
- }
- });
- }
-
- // Set form.io authentication.
- if (schema.authenticate && this.config.token) {
- requestOptions.headers['x-jwt-token'] = this.config.token;
- }
-
- return fetch(new Request(requestOptions.url, {
- headers: new Headers(requestOptions.headers)
- }))
- .then(response => {
- if (!response.ok) {
- return false;
- }
-
- return response.json();
- })
- .then((results) => {
- return results && results.length;
- })
- .catch(() => false);
- }
- },
- min: {
- key: 'validate.min',
- hasLabel: true,
- message(component, setting) {
- return component.t(component.errorMessage('min'), {
- field: component.errorLabel,
- min: parseFloat(setting),
- data: component.data
- });
- },
- check(component, setting, value) {
- const min = parseFloat(setting);
- const parsedValue = parseFloat(value);
-
- if (Number.isNaN(min) || Number.isNaN(parsedValue)) {
- return true;
- }
-
- return parsedValue >= min;
- }
- },
- max: {
- key: 'validate.max',
- hasLabel: true,
- message(component, setting) {
- return component.t(component.errorMessage('max'), {
- field: component.errorLabel,
- max: parseFloat(setting),
- data: component.data
- });
- },
- check(component, setting, value) {
- const max = parseFloat(setting);
- const parsedValue = parseFloat(value);
-
- if (Number.isNaN(max) || Number.isNaN(parsedValue)) {
- return true;
- }
-
- return parsedValue <= max;
- }
- },
- minSelectedCount: {
- key: 'validate.minSelectedCount',
- message(component, setting) {
- return component.component.minSelectedCountMessage
- ? component.component.minSelectedCountMessage
- : component.t(component.errorMessage('minSelectedCount'), {
- minCount: parseFloat(setting),
- data: component.data
- });
- },
- check(component, setting, value) {
- const min = parseFloat(setting);
-
- if (!min) {
- return true;
- }
- const count = Object.keys(value).reduce((total, key) =>{
- if (value[key]) {
- total++;
- }
- return total;
- }, 0);
-
- // Should not be triggered if there is no options selected at all
- return !count || count >= min;
- }
- },
- maxSelectedCount: {
- key: 'validate.maxSelectedCount',
- message(component, setting) {
- return component.component.maxSelectedCountMessage
- ? component.component.maxSelectedCountMessage
- : component.t(component.errorMessage('maxSelectedCount'), {
- minCount: parseFloat(setting),
- data: component.data
- });
- },
- check(component, setting, value) {
- const max = parseFloat(setting);
-
- if (!max) {
- return true;
- }
- const count = Object.keys(value).reduce((total, key) =>{
- if (value[key]) {
- total++;
- }
- return total;
- }, 0);
-
- return count <= max;
- }
- },
- minLength: {
- key: 'validate.minLength',
- hasLabel: true,
- message(component, setting) {
- return component.t(component.errorMessage('minLength'), {
- field: component.errorLabel,
- length: setting,
- data: component.data
- });
- },
- check(component, setting, value) {
- const minLength = parseInt(setting, 10);
- if (!value || !minLength || (typeof value !== 'string') || component.isEmpty(value)) {
- return true;
- }
- return (value.length >= minLength);
- }
- },
- maxLength: {
- key: 'validate.maxLength',
- hasLabel: true,
- message(component, setting) {
- return component.t(component.errorMessage('maxLength'), {
- field: component.errorLabel,
- length: setting,
- data: component.data
- });
- },
- check(component, setting, value) {
- const maxLength = parseInt(setting, 10);
- if (!maxLength || (typeof value !== 'string')) {
- return true;
- }
- return (value.length <= maxLength);
- }
- },
- maxWords: {
- key: 'validate.maxWords',
- hasLabel: true,
- message(component, setting) {
- return component.t(component.errorMessage('maxWords'), {
- field: component.errorLabel,
- length: setting,
- data: component.data
- });
- },
- check(component, setting, value) {
- const maxWords = parseInt(setting, 10);
- if (!maxWords || (typeof value !== 'string')) {
- return true;
- }
- return (value.trim().split(/\s+/).length <= maxWords);
- }
- },
- minWords: {
- key: 'validate.minWords',
- hasLabel: true,
- message(component, setting) {
- return component.t(component.errorMessage('minWords'), {
- field: component.errorLabel,
- length: setting,
- data: component.data
- });
- },
- check(component, setting, value) {
- const minWords = parseInt(setting, 10);
- if (!minWords || !value || (typeof value !== 'string')) {
- return true;
- }
- return (value.trim().split(/\s+/).length >= minWords);
- }
- },
- email: {
- hasLabel: true,
- message(component) {
- return component.t(component.errorMessage('invalid_email'), {
- field: component.errorLabel,
- data: component.data
- });
- },
- check(component, setting, value) {
- /* eslint-disable max-len */
- // From http://stackoverflow.com/questions/46155/validate-email-address-in-javascript
- const re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
- /* eslint-enable max-len */
-
- // Allow emails to be valid if the component is pristine and no value is provided.
- return !value || re.test(value);
- }
- },
- url: {
- hasLabel: true,
- message(component) {
- return component.t(component.errorMessage('invalid_url'), {
- field: component.errorLabel,
- data: component.data
- });
- },
- check(component, setting, value) {
- /* eslint-disable max-len */
- // From https://stackoverflow.com/questions/8667070/javascript-regular-expression-to-validate-url
- const re = /^(?:(?:(?:https?|ftp):)?\/\/)?(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:[/?#]\S*)?$/i;
- // From http://stackoverflow.com/questions/46155/validate-email-address-in-javascript
- const emailRe = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
- /* eslint-enable max-len */
-
- // Allow urls to be valid if the component is pristine and no value is provided.
- return !value || (re.test(value) && !emailRe.test(value));
- }
- },
- date: {
- hasLabel: true,
- message(component) {
- return component.t(component.errorMessage('invalid_date'), {
- field: component.errorLabel,
- data: component.data
- });
- },
- check(component, setting, value) {
- if (!value) {
- return true;
- }
- if (value === 'Invalid date' || value === 'Invalid Date') {
- return false;
- }
- if (typeof value === 'string') {
- value = new Date(value);
- }
- return value instanceof Date === true && value.toString() !== 'Invalid Date';
- }
- },
- day: {
- hasLabel: true,
- message(component) {
- return component.t(component.errorMessage('invalid_day'), {
- field: component.errorLabel,
- data: component.data
- });
- },
- check(component, setting, value) {
- if (!value) {
- return true;
- }
- const [DAY, MONTH, YEAR] = component.dayFirst ? [0, 1, 2] : [1, 0, 2];
- const values = value.split('/').map(x => parseInt(x, 10)),
- day = values[DAY],
- month = values[MONTH],
- year = values[YEAR],
- maxDay = getDaysInMonthCount(month, year);
-
- if (day < 0 || day > maxDay) {
- return false;
- }
- if (month < 0 || month > 12) {
- return false;
- }
- if (year < 0 || year > 9999) {
- return false;
- }
- return true;
-
- function isLeapYear(year) {
- // Year is leap if it is evenly divisible by 400 or evenly divisible by 4 and not evenly divisible by 100.
- return !(year % 400) || (!!(year % 100) && !(year % 4));
- }
-
- function getDaysInMonthCount(month, year) {
- switch (month) {
- case 1: // January
- case 3: // March
- case 5: // May
- case 7: // July
- case 8: // August
- case 10: // October
- case 12: // December
- return 31;
- case 4: // April
- case 6: // June
- case 9: // September
- case 11: // November
- return 30;
- case 2: // February
- return isLeapYear(year) ? 29 : 28;
- default:
- return 31;
- }
- }
- }
- },
- pattern: {
- key: 'validate.pattern',
- hasLabel: true,
- message(component, setting) {
- return component.t(_.get(component, 'component.validate.patternMessage', component.errorMessage('pattern')), {
- field: component.errorLabel,
- pattern: setting,
- data: component.data
- });
- },
- check(component, setting, value) {
- if (component.isEmpty(value)) return true;
-
- const pattern = setting;
- if (!pattern) {
- return true;
- }
- const regex = new RegExp(`^${pattern}$`);
- return regex.test(value);
- }
- },
- json: {
- key: 'validate.json',
- check(component, setting, value, data, index, row) {
- if (!setting) {
- return true;
- }
- const valid = component.evaluate(setting, {
- data,
- row,
- rowIndex: index,
- input: value
- });
- if (valid === null) {
- return true;
- }
- return valid;
- }
- },
- mask: {
- key: 'inputMask',
- hasLabel: true,
- message(component) {
- return component.t(component.errorMessage('mask'), {
- field: component.errorLabel,
- data: component.data
- });
- },
- check(component, setting, value) {
- let inputMask;
- if (component.isMultipleMasksField) {
- const maskName = value ? value.maskName : undefined;
- const formioInputMask = component.getMaskByName(maskName);
- if (formioInputMask) {
- inputMask = formioInputMask;
- }
- value = value ? value.value : value;
- }
- else {
- inputMask = setting;
- }
-
- if (value && inputMask && typeof value === 'string' && component.type === 'textfield' ) {
- return Inputmask.isValid(value, inputMask);
- }
-
- inputMask = inputMask ? getInputMask(inputMask) : null;
-
- if (value && inputMask && !component.skipMaskValidation) {
- // If char which is used inside mask placeholder was used in the mask, replace it with space to prevent errors
- inputMask = inputMask.map((char) => char === component.placeholderChar ? ' ' : char);
- return matchInputMask(value, inputMask);
- }
-
- return true;
- }
- },
- custom: {
- key: 'validate.custom',
- message(component) {
- return component.t(component.errorMessage('custom'), {
- field: component.errorLabel,
- data: component.data
- });
- },
- check(component, setting, value, data, index, row) {
- if (!setting) {
- return true;
- }
- const valid = component.evaluate(setting, {
- valid: true,
- data,
- rowIndex: index,
- row,
- input: value
- }, 'valid', true);
- if (valid === null) {
- return true;
- }
- return valid;
- }
- },
- maxDate: {
- key: 'maxDate',
- hasLabel: true,
- message(component, setting) {
- const date = getDateSetting(setting);
- return component.t(component.errorMessage('maxDate'), {
- field: component.errorLabel,
- maxDate: moment(date).format(component.format),
- });
- },
- check(component, setting, value) {
- //if any parts of day are missing, skip maxDate validation
- if (component.isPartialDay && component.isPartialDay(value)) {
- return true;
- }
- const date = component.getValidationFormat ? moment(value, component.getValidationFormat()) : moment(value);
- const maxDate = getDateSetting(setting);
-
- if (_.isNull(maxDate)) {
- return true;
- }
- else {
- maxDate.setHours(0, 0, 0, 0);
- }
-
- return date.isBefore(maxDate) || date.isSame(maxDate);
- }
- },
- minDate: {
- key: 'minDate',
- hasLabel: true,
- message(component, setting) {
- const date = getDateSetting(setting);
- return component.t(component.errorMessage('minDate'), {
- field: component.errorLabel,
- minDate: moment(date).format(component.format),
- });
- },
- check(component, setting, value) {
- //if any parts of day are missing, skip minDate validation
- if (component.isPartialDay && component.isPartialDay(value)) {
- return true;
- }
- const date = component.getValidationFormat ? moment(value, component.getValidationFormat()) : moment(value);
- const minDate = getDateSetting(setting);
- if (_.isNull(minDate)) {
- return true;
- }
- else {
- minDate.setHours(0, 0, 0, 0);
- }
-
- return date.isAfter(minDate) || date.isSame(minDate);
- }
- },
- minYear: {
- key: 'minYear',
- hasLabel: true,
- message(component, setting) {
- return component.t(component.errorMessage('minYear'), {
- field: component.errorLabel,
- minYear: setting,
- });
- },
- check(component, setting, value) {
- const minYear = setting;
- let year = /\d{4}$/.exec(value);
- year = year ? year[0] : null;
-
- if (!(+minYear) || !(+year)) {
- return true;
- }
-
- return +year >= +minYear;
- }
- },
- maxYear: {
- key: 'maxYear',
- hasLabel: true,
- message(component, setting) {
- return component.t(component.errorMessage('maxYear'), {
- field: component.errorLabel,
- maxYear: setting,
- });
- },
- check(component, setting, value) {
- const maxYear = setting;
- let year = /\d{4}$/.exec(value);
- year = year ? year[0] : null;
-
- if (!(+maxYear) || !(+year)) {
- return true;
- }
-
- return +year <= +maxYear;
- }
- },
- calendar: {
- key: 'validate.calendar',
- messageText: '',
- hasLabel: true,
- message(component) {
- return component.t(component.errorMessage(this.validators.calendar.messageText), {
- field: component.errorLabel,
- maxDate: moment(component.dataValue).format(component.format),
- });
- },
- check(component, setting, value, data, index) {
- this.validators.calendar.messageText = '';
- const widget = component.getWidget(index);
- if (!widget) {
- return true;
- }
- const { settings, enteredDate } = widget;
- const { minDate, maxDate, format } = settings;
- const momentFormat = [convertFormatToMoment(format)];
-
- if (momentFormat[0].match(/M{3,}/g)) {
- momentFormat.push(momentFormat[0].replace(/M{3,}/g, 'MM'));
- }
-
- if (!value && enteredDate) {
- const { message, result } = checkInvalidDate(enteredDate, momentFormat, minDate, maxDate);
-
- if (!result) {
- this.validators.calendar.messageText = message;
- return result;
- }
- }
-
- if (value && enteredDate) {
- if (moment(value).format() !== moment(enteredDate, momentFormat, true).format() && enteredDate.match(/_/gi)) {
- this.validators.calendar.messageText = CALENDAR_ERROR_MESSAGES.INCOMPLETE;
- return false;
- }
- else {
- widget.enteredDate = '';
- return true;
- }
- }
- }
- },
- time: {
- key: 'validate.time',
- messageText: 'Invalid time',
- hasLabel: true,
- message(component) {
- return component.t(component.errorMessage(this.validators.time.messageText), {
- field: component.errorLabel
- });
- },
- check(component, setting, value) {
- if (component.isEmpty(value)) return true;
- return moment(value, component.component.format).isValid();
- }
- },
- availableValueProperty: {
- key: 'validate.availableValueProperty',
- method: 'validateValueProperty',
- messageText: 'Invalid Value Property',
- hasLabel: true,
- message(component) {
- return component.t(component.errorMessage(this.validators.availableValueProperty.messageText), {
- field: component.errorLabel,
- });
- },
- check(component, setting, value) {
- if (component.component.dataSrc === 'url' && (_.isUndefined(value) || _.isObject(value))) {
- return false;
- }
-
- return true;
- }
- }
- };
- }
-
- checkValidator(component, validator, setting, value, data, index, row, async) {
- let resultOrPromise = null;
-
- // Allow each component to override their own validators by implementing the validator.method
- if (validator.method && (typeof component[validator.method] === 'function')) {
- resultOrPromise = component[validator.method](setting, value, data, index, row, async);
- }
- else {
- resultOrPromise = validator.check.call(this, component, setting, value, data, index, row, async);
- }
-
- const processResult = result => {
- if (typeof result === 'string') {
- return result;
- }
-
- if (!result && validator.message) {
- return validator.message.call(this, component, setting, index, row);
- }
-
- return '';
- };
-
- if (async) {
- return Promise.resolve(resultOrPromise).then(processResult);
- }
- else {
- return processResult(resultOrPromise);
- }
- }
-
- validate(component, validatorName, value, data, index, row, async, conditionallyVisible, validationObj) {
- // Skip validation for conditionally hidden components
- if (!conditionallyVisible) {
- return false;
- }
-
- const validator = this.validators[validatorName];
- const setting = _.get(validationObj || component.component, validator.key, null);
- const resultOrPromise = this.checkValidator(component, validator, setting, value, data, index, row, async);
-
- const processResult = result => {
- if (result) {
- const resultData = {
- message: unescapeHTML(_.get(result, 'message', result)),
- level: _.get(result, 'level') === 'warning' ? 'warning' : 'error',
- path: getArrayFromComponentPath(component.path || ''),
- context: {
- validator: validatorName,
- hasLabel: validator.hasLabel,
- setting,
- key: component.key,
- label: component.label,
- value,
- index,
- input: component.refs.input?.[index]
- }
- };
- if (validatorName ==='unique' && component.conflictId) {
- resultData.conflictId = component.conflictId;
- }
- return resultData;
- }
- else {
- return false;
- }
- };
-
- if (async) {
- return Promise.resolve(resultOrPromise).then(processResult);
- }
- else {
- return processResult(resultOrPromise);
- }
- }
-
- checkComponent(component, data, row, includeWarnings = false, async = false) {
- const isServerSidePersistent = typeof process !== 'undefined'
- && _.get(process, 'release.name') === 'node'
- && !_.defaultTo(component.component.persistent, true);
-
- // If we're server-side and it's not a persistent component, don't run validation at all
- if (isServerSidePersistent || component.component.validate === false) {
- return async ? Promise.resolve([]) : [];
- }
-
- data = data || component.rootValue;
- row = row || component.data;
-
- const values = (component.component.multiple && Array.isArray(component.validationValue))
- ? component.validationValue
- : [component.validationValue];
- const conditionallyVisible = component.conditionallyVisible();
- const addonsValidations = [];
-
- if (component?.addons?.length) {
- values.forEach((value) => {
- component.addons.forEach((addon) => {
- if (!addon.checkValidity(value)) {
- addonsValidations.push(...(addon.errors || []));
- }
- });
- });
- }
-
- // If this component has the new validation system enabled, use it instead.
- const validations = _.get(component, 'component.validations');
- let nextGenResultsOrPromises = [];
-
- if (validations && Array.isArray(validations) && validations.length) {
- const validationsGroupedByMode = _.chain(validations)
- .groupBy((validation) => validation.mode)
- .value();
-
- if (component.calculateCondition) {
- includeWarnings = true;
-
- const uiGroupedValidation = _.chain(validationsGroupedByMode.ui)
- .filter('active')
- .groupBy((validation) => validation.group || null)
- .value();
-
- const commonValidations = uiGroupedValidation.null || [];
- delete uiGroupedValidation.null;
-
- commonValidations.forEach(({ condition, message, severity }) => {
- if (!component.calculateCondition(condition)) {
- nextGenResultsOrPromises.push({
- level: severity || 'error',
- message: component.t(message),
- componentInstance: component,
- });
- }
- });
-
- _.forEach(uiGroupedValidation, (validationGroup) => {
- _.forEach(validationGroup, ({ condition, message, severity }) => {
- if (!component.calculateCondition(condition)) {
- nextGenResultsOrPromises.push({
- level: severity || 'error',
- message: component.t(message),
- componentInstance: component,
- });
-
- return false;
- }
- });
- });
- }
- else {
- nextGenResultsOrPromises = this.checkValidations(component, validations, data, row, values, async);
- }
- if (component.validators.includes('custom') && validationsGroupedByMode.js) {
- _.each(validationsGroupedByMode.js, (validation) => {
- nextGenResultsOrPromises.push(_.map(values, (value, index) => this.validate(component, 'custom', value, data, index, row, async, conditionallyVisible, validation)));
- });
- }
- if (component.validators.includes('json') && validationsGroupedByMode.json) {
- _.each(validationsGroupedByMode.json, (validation) => {
- nextGenResultsOrPromises.push(_.map(values, (value, index) => this.validate(component, 'json', value, data, index, row, async, conditionallyVisible, validation)));
- });
- }
- }
-
- const validateCustom = _.get(component, 'component.validate.custom');
- const customErrorMessage = _.get(component, 'component.validate.customMessage');
-
- // Run primary validators
- const resultsOrPromises = _(component.validators).chain()
- .map(validatorName => {
- if (!this.validators.hasOwnProperty(validatorName)) {
- return {
- message: `Validator for "${validatorName}" is not defined`,
- level: 'warning',
- context: {
- validator: validatorName,
- key: component.key,
- label: component.label
- }
- };
- }
-
- // Handle the case when there is no values defined and it is required.
- if (validatorName === 'required' && !values.length) {
- return [this.validate(component, validatorName, null, data, 0, row, async, conditionallyVisible)];
- }
-
- return _.map(values, (value, index) => this.validate(component, validatorName, value, data, index, row, async, conditionallyVisible));
- })
- .flatten()
- .value();
-
- // Run the "unique" pseudo-validator
- component.component.validate = component.component.validate || {};
- component.component.validate.unique = component.component.unique;
- resultsOrPromises.push(this.validate(component, 'unique', component.validationValue, data, 0, data, async, conditionallyVisible));
-
- // Run the "multiple" pseudo-validator
- component.component.validate.multiple = component.component.multiple;
- resultsOrPromises.push(this.validate(component, 'multiple', component.validationValue, data, 0, data, async, conditionallyVisible));
-
- resultsOrPromises.push(...addonsValidations);
- resultsOrPromises.push(...nextGenResultsOrPromises);
-
- // Define how results should be formatted
- const formatResults = results => {
- // Condense to a single flat array
- results = _(results).chain().flatten().compact().value();
-
- if (customErrorMessage || validateCustom) {
- _.each(results, result => {
- result.message = component.t(customErrorMessage || result.message, {
- field: component.errorLabel,
- data,
- row,
- error: result
- });
- result.context.hasLabel = false;
- });
- }
-
- return includeWarnings ? results : _.reject(results, result => result.level === 'warning');
- };
- // Wait for results if using async mode, otherwise process and return immediately
- if (async) {
- return Promise.all(resultsOrPromises).then(formatResults);
- }
- else {
- return formatResults(resultsOrPromises);
- }
- }
-
- /**
- * Use the new validations engine to evaluate any errors.
- *
- * @param component
- * @param validations
- * @param data
- * @param row
- * @param values
- * @returns {any[]}
- */
- checkValidations(component, validations, data, row, values, async) {
- // Get results.
- const results = validations.map((validation) => {
- return this.checkRule(component, validation, data, row, values, async);
- });
-
- // Flatten array and filter out empty results.
- const messages = results.reduce((prev, result) => {
- if (result) {
- return [...prev, ...result];
- }
- return prev;
- }, []).filter((result) => result);
-
- // Keep only the last error for each rule.
- const rules = messages.reduce((prev, message) => {
- prev[message.context.validator] = message;
- return prev;
- }, {});
-
- return Object.values(rules);
- }
-
- checkRule(component, validation, data, row, values, async) {
- const Rule = Rules.getRule(validation.rule);
- const results = [];
- if (Rule) {
- const rule = new Rule(component, validation.settings, this.config);
- values.map((value, index) => {
- const result = rule.check(value, data, row, async);
- if (result !== true) {
- results.push({
- level: validation.level || 'error',
- message: component.t(validation.message || rule.defaultMessage, {
- settings: validation.settings,
- field: component.errorLabel,
- data,
- row,
- error: result,
- }),
- context: {
- key: component.key,
- index,
- label: component.label,
- validator: validation.rule,
- },
- });
- }
- });
- }
- // If there are no results, return false so it is removed by filter.
- return results.length === 0 ? false : results;
- }
-
- get check() {
- return this.checkComponent;
- }
-
- get() {
- _.get.call(this, arguments);
- }
-
- each() {
- _.each.call(this, arguments);
- }
-
- has() {
- _.has.call(this, arguments);
- }
-}
-
-ValidationChecker.config = {
- db: null,
- token: null,
- form: null,
- submission: null
-};
-
-const instance = new ValidationChecker();
-
-export {
- instance as default,
- ValidationChecker
-};
diff --git a/src/validator/Validator.unit.js b/src/validator/Validator.unit.js
deleted file mode 100644
index 74242b19fe..0000000000
--- a/src/validator/Validator.unit.js
+++ /dev/null
@@ -1,1312 +0,0 @@
-'use strict';
-import DateTimeComponent from '../components/datetime/DateTime';
-import { comp1 } from '../components/datetime/fixtures';
-import Harness from '../../test/harness';
-import Validator from './Validator';
-import Component from '../components/_classes/component/Component';
-import assert from 'power-assert';
-
-describe('Legacy Validator Tests', () => {
- const baseComponent = new Component({});
-
- it('Should test for minLength', () => {
- assert.equal(Validator.validators.minLength.check(baseComponent, 5, 'test'), false);
- assert.equal(Validator.validators.minLength.check(baseComponent, 4, 'test'), true);
- assert.equal(Validator.validators.minLength.check(baseComponent, 3, 'test'), true);
- assert.equal(Validator.validators.minLength.check(baseComponent, 6, 'test'), false);
- assert.equal(Validator.validators.minLength.check(baseComponent, 6, ''), true);
- });
-
- it('Should test for maxLength', () => {
- assert.equal(Validator.validators.maxLength.check(baseComponent, 5, 'test'), true);
- assert.equal(Validator.validators.maxLength.check(baseComponent, 4, 'test'), true);
- assert.equal(Validator.validators.maxLength.check(baseComponent, 3, 'test'), false);
- assert.equal(Validator.validators.maxLength.check(baseComponent, 6, 'test'), true);
- assert.equal(Validator.validators.maxLength.check(baseComponent, 6, ''), true);
- });
-
- it('Should test for email', () => {
- assert.equal(Validator.validators.email.check(baseComponent, '', 'test'), false);
- assert.equal(Validator.validators.email.check(baseComponent, '', 'test@a'), false);
- assert.equal(Validator.validators.email.check(baseComponent, '', 'test@example.com'), true);
- assert.equal(Validator.validators.email.check(baseComponent, '', 'test@a.com'), true);
- assert.equal(Validator.validators.email.check(baseComponent, '', 'test@a.co'), true);
- });
-
- it('Should test for required', () => {
- assert.equal(Validator.validators.required.check(baseComponent, true, ''), false);
- assert.equal(Validator.validators.required.check(baseComponent, true, 't'), true);
- assert.equal(Validator.validators.required.check(baseComponent, false, ''), true);
- assert.equal(Validator.validators.required.check(baseComponent, false, 'tes'), true);
- assert.equal(Validator.validators.required.check(baseComponent, true, undefined), false);
- assert.equal(Validator.validators.required.check(baseComponent, true, null), false);
- assert.equal(Validator.validators.required.check(baseComponent, true, []), false);
- assert.equal(Validator.validators.required.check(baseComponent, true, ['test']), true);
- });
-
- it('Should test for custom', () => {
- assert.equal(Validator.validators.custom.check(baseComponent, 'valid = (input == "test")', 'test'), true);
- assert.equal(Validator.validators.custom.check(baseComponent, 'valid = (input == "test")', 'test2'), false);
- assert.equal(Validator.validators.custom.check(baseComponent, 'valid = (input == "test") ? true : "Should be false."', 'test2'), 'Should be false.');
- assert.equal(Validator.validators.custom.check(baseComponent, 'valid = (input == "test") ? true : "Should be false."', 'test'), true);
- });
-
- it('Should test for pattern', () => {
- assert.equal(Validator.validators.pattern.check(baseComponent, 'A.*', 'A'), true);
- assert.equal(Validator.validators.pattern.check(baseComponent, 'A.*', 'Aaaa'), true);
- assert.equal(Validator.validators.pattern.check(baseComponent, 'w+', 'test'), false);
- assert.equal(Validator.validators.pattern.check(baseComponent, '\\w+', 'test'), true);
- assert.equal(Validator.validators.pattern.check(baseComponent, '\\w+@\\w+', 'test@a'), true);
- assert.equal(Validator.validators.pattern.check(baseComponent, '\\w+@\\w+', 'test@example.com'), false);
- });
-
- it('Should test for json', () => {
- assert.equal(Validator.validators.json.check(baseComponent, {
- or: [{ '_isEqual': [{ var: 'data.test' }, ['1', '2', '3']] }, 'Should be false.']
- }, null, { test: ['1', '2', '3'] }), true);
- assert.equal(Validator.validators.json.check(baseComponent, {
- or: [{ '_isEqual': [{ var: 'data.test' }, ['1', '2', '3']] }, 'Should be false.']
- }, null, { test: ['1', '2', '4'] }), 'Should be false.');
- });
-
- it('Should test for date', (done) => {
- Harness.testCreate(DateTimeComponent, comp1).then((dateTime) => {
- const pass = [];
- const assertFail = (checkResults, message = 'Should fail') => {
- assert.equal(checkResults?.length, 1, message);
- assert.equal(checkResults[0].message, 'Date is not a valid date.', message);
- };
-
- dateTime.dataValue = '01/02/2000';
- assert.deepEqual(Validator.checkComponent(dateTime, {}), pass, 'Should be valid');
- dateTime.dataValue = 'January 23, 2012';
- assert.deepEqual(Validator.checkComponent(dateTime, {}), pass, 'Should be valid');
- dateTime.dataValue = '2010-10-10T10:10:10.626Z';
- assert.deepEqual(Validator.checkComponent(dateTime, {}), pass, 'Should be valid');
- dateTime.dataValue = new Date();
- assert.deepEqual(Validator.checkComponent(dateTime, {}), pass, 'Should be valid');
- dateTime.dataValue = null;
- assert.deepEqual(Validator.checkComponent(dateTime, {}), pass, 'Should be valid');
- dateTime.dataValue = undefined;
- assert.deepEqual(Validator.checkComponent(dateTime, {}), pass, 'Should be valid');
- dateTime.dataValue = '';
- assert.deepEqual(Validator.checkComponent(dateTime, {}), pass, 'Should be valid');
- dateTime.dataValue = 'Some string';
- assertFail(Validator.checkComponent(dateTime, {}), 'Should fail with a string');
- dateTime.dataValue = new Date('Some string');
- assertFail(Validator.checkComponent(dateTime, {}), 'Should fail with an invalid Date object');
- done();
- }).catch(done);
- });
-});
-
-describe('Validator Tests', () => {
- it('Validates for required', (done) => {
- const component = new Component({
- key: 'test',
- label: 'Test',
- validations: [
- {
- rule: 'required',
- }
- ]
- });
- assert.deepEqual(Validator.checkComponent(component, {}), [
- {
- context: {
- index: 0,
- key: 'test',
- label: 'Test',
- validator: 'required',
- },
- level: 'error',
- message: 'Test is required',
- }
- ]);
- done();
- });
-
- it('Overrides the message and level', (done) => {
- const component = new Component({
- key: 'test',
- label: 'Test',
- validations: [
- {
- rule: 'required',
- level: 'info',
- message: 'ABC',
- }
- ]
- });
- assert.deepEqual(Validator.checkComponent(component, {}, {}, true), [
- {
- context: {
- index: 0,
- key: 'test',
- label: 'Test',
- validator: 'required',
- },
- level: 'info',
- message: 'ABC',
- }
- ]);
- done();
- });
-
- it('Only returns the last message for a rule', (done) => {
- const component = new Component({
- key: 'test',
- label: 'Test',
- validations: [
- {
- rule: 'required',
- level: 'info',
- message: 'ABC',
- },
- {
- rule: 'required',
- level: 'error',
- message: 'DEF',
- }
- ]
- });
- assert.deepEqual(Validator.checkComponent(component, {}), [
- {
- context: {
- index: 0,
- key: 'test',
- label: 'Test',
- validator: 'required',
- },
- level: 'error',
- message: 'DEF',
- }
- ]);
- done();
- });
-
- it('Fulfills custom validation', (done) => {
- const fail = [
- {
- context: {
- index: 0,
- key: 'test',
- label: 'Test',
- validator: 'custom',
- },
- level: 'error',
- message: 'DEF',
- }
- ];
-
- const pass = [];
-
- const component = new Component({
- key: 'test',
- label: 'Test',
- validations: [
- {
- rule: 'custom',
- level: 'error',
- message: 'DEF',
- settings: {
- custom: 'valid = input === "foo";',
- }
- }
- ]
- });
- // Null is empty value so false passes for Component.
- component.dataValue = 'foo';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = 'bar';
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = 'a';
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = undefined;
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = null;
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
-
- done();
- });
-
- it('Fulfills custom validation (multiple)', (done) => {
- const fail = [
- {
- context: {
- index: 0,
- key: 'test',
- label: 'Test',
- validator: 'custom',
- },
- level: 'error',
- message: 'DEF',
- }
- ];
- const pass = [];
- const component = new Component({
- key: 'test',
- label: 'Test',
- multiple: true,
- validations: [
- {
- rule: 'custom',
- level: 'error',
- message: 'DEF',
- settings: {
- custom: 'valid = input === "foo";',
- }
- }
- ]
- });
-
- component.dataValue = [];
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = ['test'];
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
-
- done();
- });
-
- it('Fulfills date validation', (done) => {
- const fail = [
- {
- context: {
- index: 0,
- key: 'test',
- label: 'Test',
- validator: 'date',
- },
- level: 'error',
- message: 'DEF',
- }
- ];
-
- const pass = [];
-
- const component = new Component({
- key: 'test',
- label: 'Test',
- validations: [
- {
- rule: 'date',
- level: 'error',
- message: 'DEF',
- settings: {}
- }
- ]
- });
- component.dataValue = '01/05/12';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = 'January 5, 2012';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = '2019-12-04T16:33:10.626Z';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = new Date();
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = 'a';
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = undefined;
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = null;
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = '';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
-
- done();
- });
-
- it('Fulfills day validation', (done) => {
- const fail = [
- {
- context: {
- index: 0,
- key: 'test',
- label: 'Test',
- validator: 'day',
- },
- level: 'error',
- message: 'DEF',
- }
- ];
-
- const pass = [];
-
- const component = new Component({
- key: 'test',
- label: 'Test',
- validations: [
- {
- rule: 'day',
- level: 'error',
- message: 'DEF',
- settings: {}
- }
- ]
- });
- component.dataValue = '01/05/2012';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = 'January 5, 2012';
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = '2019-12-04T16:33:10.626Z';
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = new Date();
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = 'a';
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = undefined;
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = null;
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = '';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
-
- done();
- });
-
- it('Fulfills email validation', (done) => {
- const fail = [
- {
- context: {
- index: 0,
- key: 'test',
- label: 'Test',
- validator: 'email',
- },
- level: 'error',
- message: 'DEF',
- }
- ];
-
- const pass = [];
-
- const component = new Component({
- key: 'test',
- label: 'Test',
- validations: [
- {
- rule: 'email',
- level: 'error',
- message: 'DEF',
- settings: {}
- }
- ]
- });
- component.dataValue = '';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = 'test';
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = 'test@example';
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = 'test@example.com';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = 'test.test@example.com';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = 'test.test@example.io';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = undefined;
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = null;
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
-
- done();
- });
-
- it('Fulfills json validation', (done) => {
- const fail = [
- {
- context: {
- index: 0,
- key: 'test',
- label: 'Test',
- validator: 'json',
- },
- level: 'error',
- message: 'DEF',
- }
- ];
-
- const pass = [];
-
- const component = new Component({
- key: 'test',
- label: 'Test',
- validations: [
- {
- rule: 'json',
- level: 'error',
- message: 'DEF',
- settings: {
- json: { '==' : [{ var: 'input' }, 'foo'] },
- }
- }
- ]
- });
- component.dataValue = 'foo';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = 'bar';
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = 'a';
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = undefined;
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = null;
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
-
- done();
- });
-
- it('Fulfills json validation (multiple)', (done) => {
- const fail = [
- {
- context: {
- index: 0,
- key: 'test',
- label: 'Test',
- validator: 'json',
- },
- level: 'error',
- message: 'DEF',
- }
- ];
-
- const pass = [];
-
- const component = new Component({
- key: 'test',
- label: 'Test',
- multiple: true,
- validations: [
- {
- rule: 'json',
- level: 'error',
- message: 'DEF',
- settings: {
- json: { '==' : [{ var: 'input' }, 'foo'] },
- }
- }
- ]
- });
-
- component.dataValue = [];
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
-
- component.dataValue = ['test'];
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
-
- done();
- });
-
- it('Fulfills mask validation', (done) => {
- const fail = [
- {
- context: {
- index: 0,
- key: 'test',
- label: 'Test',
- validator: 'mask',
- },
- level: 'error',
- message: 'DEF',
- }
- ];
-
- const pass = [];
-
- const component = new Component({
- key: 'test',
- label: 'Test',
- validations: [
- {
- rule: 'mask',
- level: 'error',
- message: 'DEF',
- settings: {
- mask: '999',
- }
- }
- ]
- });
- component.dataValue = '123';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = 'aaa';
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = '12';
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = '1234';
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = '1a2';
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = undefined;
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = null;
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = '';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
-
- done();
- });
-
- it('Fulfills max validation', (done) => {
- const fail = [
- {
- context: {
- index: 0,
- key: 'test',
- label: 'Test',
- validator: 'max',
- },
- level: 'error',
- message: 'DEF',
- }
- ];
-
- const pass = [];
-
- const component = new Component({
- key: 'test',
- label: 'Test',
- validations: [
- {
- rule: 'max',
- level: 'error',
- message: 'DEF',
- settings: {
- limit: '10',
- }
- }
- ]
- });
- component.dataValue = -1;
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = 0;
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = 1;
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = 9;
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = 10;
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = 11;
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = 1000000000;
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = '12';
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = undefined;
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = null;
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = '';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
-
- done();
- });
-
- it('Fulfills maxDate validation', (done) => {
- const fail = [
- {
- context: {
- index: 0,
- key: 'test',
- label: 'Test',
- validator: 'maxDate',
- },
- level: 'error',
- message: 'DEF',
- }
- ];
-
- const pass = [];
-
- const component = new Component({
- key: 'test',
- label: 'Test',
- validations: [
- {
- rule: 'maxDate',
- level: 'error',
- message: 'DEF',
- settings: {
- dateLimit: '2019-12-04T00:00:00.000Z',
- }
- }
- ]
- });
- component.dataValue = '2010-12-03T00:00:00.000Z';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = '2019-12-03T00:00:00.000Z';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = '2019-12-04T00:00:00.000Z';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = '2019-12-05T00:00:00.000Z';
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = '2029-12-05T00:00:00.000Z';
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = undefined;
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = null;
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = '';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
-
- done();
- });
-
- it('Fulfills maxLength validation', (done) => {
- const fail = [
- {
- context: {
- index: 0,
- key: 'test',
- label: 'Test',
- validator: 'maxLength',
- },
- level: 'error',
- message: 'DEF',
- }
- ];
-
- const pass = [];
-
- const component = new Component({
- key: 'test',
- label: 'Test',
- validations: [
- {
- rule: 'maxLength',
- level: 'error',
- message: 'DEF',
- settings: {
- length: '10',
- }
- }
- ]
- });
- component.dataValue = 'a';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = '123456789';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = '123456789a';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = '1234567890a';
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = 'this is a really long string';
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = undefined;
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = null;
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = '';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
-
- done();
- });
-
- it('Fulfills maxWords validation', (done) => {
- const fail = [
- {
- context: {
- index: 0,
- key: 'test',
- label: 'Test',
- validator: 'maxWords',
- },
- level: 'error',
- message: 'DEF',
- }
- ];
-
- const pass = [];
-
- const component = new Component({
- key: 'test',
- label: 'Test',
- validations: [
- {
- rule: 'maxWords',
- level: 'error',
- message: 'DEF',
- settings: {
- length: '3',
- }
- }
- ]
- });
- component.dataValue = 'This';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = 'This is';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = 'This is a';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = 'This is a test';
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = 'this is a really long string';
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = undefined;
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = null;
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = '';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
-
- done();
- });
-
- it('Fulfills maxYear validation', (done) => {
- const fail = [
- {
- context: {
- index: 0,
- key: 'test',
- label: 'Test',
- validator: 'maxYear',
- },
- level: 'error',
- message: 'DEF',
- }
- ];
-
- const pass = [];
-
- const component = new Component({
- key: 'test',
- label: 'Test',
- validations: [
- {
- rule: 'maxYear',
- level: 'error',
- message: 'DEF',
- settings: '2020'
- }
- ]
- });
- component.dataValue = '2030';
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = '2021';
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = '3040';
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = '0000';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = '2000';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = undefined;
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = null;
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = '';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
-
- done();
- });
-
- it('Fulfills minYear validation', (done) => {
- const fail = [
- {
- context: {
- index: 0,
- key: 'test',
- label: 'Test',
- validator: 'minYear',
- },
- level: 'error',
- message: 'DEF',
- }
- ];
-
- const pass = [];
-
- const component = new Component({
- key: 'test',
- label: 'Test',
- validations: [
- {
- rule: 'minYear',
- level: 'error',
- message: 'DEF',
- settings: '2000'
- }
- ]
- });
- component.dataValue = '1880';
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = '0011';
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = '1990';
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = '0000';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = '2020';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = '2000';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = undefined;
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = null;
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = '';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
-
- done();
- });
-
- it('Fulfills min validation', (done) => {
- const fail = [
- {
- context: {
- index: 0,
- key: 'test',
- label: 'Test',
- validator: 'min',
- },
- level: 'error',
- message: 'DEF',
- }
- ];
-
- const pass = [];
-
- const component = new Component({
- key: 'test',
- label: 'Test',
- validations: [
- {
- rule: 'min',
- level: 'error',
- message: 'DEF',
- settings: {
- limit: '10',
- }
- }
- ]
- });
- component.dataValue = -1;
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = 0;
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = 1;
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = 9;
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = 10;
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = 11;
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = 1000000000;
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = '12';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = undefined;
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = null;
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = '';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
-
- done();
- });
-
- it('Fulfills minDate validation', (done) => {
- const fail = [
- {
- context: {
- index: 0,
- key: 'test',
- label: 'Test',
- validator: 'minDate',
- },
- level: 'error',
- message: 'DEF',
- }
- ];
-
- const pass = [];
-
- const component = new Component({
- key: 'test',
- label: 'Test',
- validations: [
- {
- rule: 'minDate',
- level: 'error',
- message: 'DEF',
- settings: {
- dateLimit: '2019-12-04T00:00:00.000Z',
- }
- }
- ]
- });
- component.dataValue = '2010-12-03T00:00:00.000Z';
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = '2019-12-03T00:00:00.000Z';
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = '2019-12-04T00:00:00.000Z';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = '2019-12-05T00:00:00.000Z';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = '2029-12-05T00:00:00.000Z';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = undefined;
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = null;
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = '';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
-
- done();
- });
-
- it('Fulfills minLength validation', (done) => {
- const fail = [
- {
- context: {
- index: 0,
- key: 'test',
- label: 'Test',
- validator: 'minLength',
- },
- level: 'error',
- message: 'DEF',
- }
- ];
-
- const pass = [];
-
- const component = new Component({
- key: 'test',
- label: 'Test',
- validations: [
- {
- rule: 'minLength',
- level: 'error',
- message: 'DEF',
- settings: {
- length: '10',
- }
- }
- ]
- });
- component.dataValue = 'a';
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = '123456789';
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = '123456789a';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = '1234567890a';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = 'this is a really long string';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = undefined;
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = null;
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = '';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
-
- done();
- });
-
- it('Fulfills minWords validation', (done) => {
- const fail = [
- {
- context: {
- index: 0,
- key: 'test',
- label: 'Test',
- validator: 'minWords',
- },
- level: 'error',
- message: 'DEF',
- }
- ];
-
- const pass = [];
-
- const component = new Component({
- key: 'test',
- label: 'Test',
- validations: [
- {
- rule: 'minWords',
- level: 'error',
- message: 'DEF',
- settings: {
- length: '3',
- }
- }
- ]
- });
- component.dataValue = 'This';
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = 'This is';
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = 'This is a';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = 'This is a test';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = 'this is a really long string';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = undefined;
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = null;
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = '';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
-
- done();
- });
-
- it('Fulfills pattern validation', (done) => {
- const fail = [
- {
- context: {
- index: 0,
- key: 'test',
- label: 'Test',
- validator: 'pattern',
- },
- level: 'error',
- message: 'DEF',
- }
- ];
-
- const pass = [];
-
- const component = new Component({
- key: 'test',
- label: 'Test',
- validations: [
- {
- rule: 'pattern',
- level: 'error',
- message: 'DEF',
- settings: {
- pattern: 'a.c',
- }
- }
- ]
- });
- component.dataValue = 'abc';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = 'adc';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = 'aaa';
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = 'ccc';
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = 'a';
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = undefined;
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = null;
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
-
- done();
- });
-
- it('Fulfills pattern validation (multiple)', (done) => {
- const fail = [
- {
- context: {
- index: 0,
- key: 'test',
- label: 'Test',
- validator: 'pattern',
- },
- level: 'error',
- message: 'DEF',
- }
- ];
-
- const pass = [];
-
- const component = new Component({
- key: 'test',
- label: 'Test',
- multiple: true,
- validations: [
- {
- rule: 'pattern',
- level: 'error',
- message: 'DEF',
- settings: {
- pattern: 'a.c',
- }
- }
- ]
- });
-
- component.dataValue = [];
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = ['abc'];
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = ['abv'];
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
-
- done();
- });
-
- it('Fulfills required validation', (done) => {
- const fail = [
- {
- context: {
- index: 0,
- key: 'test',
- label: 'Test',
- validator: 'required',
- },
- level: 'error',
- message: 'DEF',
- }
- ];
-
- const pass = [];
-
- const component = new Component({
- key: 'test',
- label: 'Test',
- validations: [
- {
- rule: 'required',
- level: 'error',
- message: 'DEF',
- }
- ]
- });
- // Null is empty value so false passes for Component.
- component.dataValue = false;
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = true;
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = 't';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = 'test';
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- component.dataValue = '';
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = undefined;
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = null;
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
-
- done();
- });
-
- it('Fulfills required validation (multiple)', (done) => {
- const fail = [
- {
- context: {
- index: 0,
- key: 'test',
- label: 'Test',
- validator: 'required',
- },
- level: 'error',
- message: 'DEF',
- }
- ];
-
- const pass = [];
-
- const component = new Component({
- key: 'test',
- label: 'Test',
- type: 'textfield',
- multiple: true,
- validations: [
- {
- rule: 'required',
- level: 'error',
- message: 'DEF',
- }
- ]
- });
-
- component.dataValue = [''];
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- component.dataValue = ['test'];
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
-
- done();
- });
-
- it('Fulfills url validation', (done) => {
- const fail = [
- {
- context: {
- index: 0,
- key: 'test',
- label: 'Test',
- validator: 'url',
- },
- level: 'error',
- message: 'DEF',
- }
- ];
-
- const pass = [];
-
- const component = new Component({
- key: 'test',
- label: 'Test',
- validations: [
- {
- rule: 'url',
- level: 'error',
- message: 'DEF',
- }
- ]
- });
-
- const valid = [
- 'test.com',
- 'http://test.com',
- 'https://test.com',
- 'https://www.test.com',
- 'https://one.two.three.four.test.io',
- 'https://www.test.com/test',
- 'https://www.test.com/test/test.html',
- 'https://www.test.com/one/two/three/four/test.html',
- 'www.example.com',
- 'http://www.example.com#up',
- 'https://wikipedia.org/@/ru',
- 'https://wikipedia.com/@',
- 'http://www.site.com:8008',
- 'ftp://www.site.com',
- undefined,
- null,
- ];
-
- const invalid = [
- 't',
- 'test',
- 'http://test',
- 'test@gmail.com',
- 'test@gmail.com ',
- 'test@gmail...com',
- 'test..com',
- 'http://test...com',
- 'http:://test.com',
- 'http:///test.com',
- 'https://www..example.com',
- ];
-
- try {
- valid.forEach((value) => {
- component.dataValue = value;
- assert.deepEqual(Validator.checkComponent(component, {}), pass);
- });
-
- invalid.forEach((value) => {
- component.dataValue = value;
- assert.deepEqual(Validator.checkComponent(component, {}), fail);
- });
-
- done();
- }
- catch (e) {
- done(e);
- }
- });
-});
diff --git a/src/validator/conjunctions/index.js b/src/validator/conjunctions/index.js
deleted file mode 100644
index 081cce9d8a..0000000000
--- a/src/validator/conjunctions/index.js
+++ /dev/null
@@ -1,19 +0,0 @@
-export default class Conjunctions {
- static conjunctions = {};
-
- static addConjunction(name, conjunction) {
- Conjunctions.conjunctions[name] = conjunction;
- }
-
- static addConjunctions(conjunctions) {
- Conjunctions.conjunctions = { ...Conjunctions.conjunctions, ...conjunctions };
- }
-
- static getConjunction(name) {
- return Conjunctions.conjunctions[name];
- }
-
- static getConjunctions() {
- return Conjunctions.conjunctions;
- }
-}
diff --git a/src/validator/operators/index.js b/src/validator/operators/index.js
deleted file mode 100644
index e863903095..0000000000
--- a/src/validator/operators/index.js
+++ /dev/null
@@ -1,19 +0,0 @@
-export default class Operators {
- static operators = {};
-
- static addOperator(name, operator) {
- Operators.operators[name] = operator;
- }
-
- static addOperators(operators) {
- Operators.operators = { ...Operators.operators, ...operators };
- }
-
- static getOperator(name) {
- return Operators.operators[name];
- }
-
- static getOperators() {
- return Operators.operators;
- }
-}
diff --git a/src/validator/quickRules/index.js b/src/validator/quickRules/index.js
deleted file mode 100644
index a3169ea0ac..0000000000
--- a/src/validator/quickRules/index.js
+++ /dev/null
@@ -1,19 +0,0 @@
-export default class QuickRules {
- static quickRules = {};
-
- static addQuickRule(name, quickRule) {
- QuickRules.quickRules[name] = quickRule;
- }
-
- static addQuickRules(quickRules) {
- QuickRules.quickRules = { ...QuickRules.quickRules, ...quickRules };
- }
-
- static getQuickRule(name) {
- return QuickRules.quickRules[name];
- }
-
- static getQuickRules() {
- return QuickRules.quickRules;
- }
-}
diff --git a/src/validator/rules/Custom.js b/src/validator/rules/Custom.js
deleted file mode 100644
index 68efec73ff..0000000000
--- a/src/validator/rules/Custom.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import Rule from './Rule';
-
-export default class Custom extends Rule {
- defaultMessage = '{{error}}';
-
- check(value, data, row, index) {
- const custom = this.settings.custom;
-
- if (!custom) {
- return true;
- }
-
- const valid = this.component.evaluate(custom, {
- valid: true,
- data,
- row,
- rowIndex: index,
- input: value,
- }, 'valid', true);
-
- if (valid === null) {
- return true;
- }
-
- return valid;
- }
-}
diff --git a/src/validator/rules/Date.js b/src/validator/rules/Date.js
deleted file mode 100644
index 3a1aa60c79..0000000000
--- a/src/validator/rules/Date.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import Rule from './Rule';
-
-export default class DateRule extends Rule {
- defaultMessage = '{{field}} is not a valid date.';
-
- check(value) {
- if (!value) {
- return true;
- }
- if (value === 'Invalid date' || value === 'Invalid Date') {
- return false;
- }
- if (typeof value === 'string') {
- value = new Date(value);
- }
- return value instanceof Date === true && value.toString() !== 'Invalid Date';
- }
-}
diff --git a/src/validator/rules/Day.js b/src/validator/rules/Day.js
deleted file mode 100644
index e5d5379540..0000000000
--- a/src/validator/rules/Day.js
+++ /dev/null
@@ -1,58 +0,0 @@
-import Rule from './Rule';
-
-export default class Day extends Rule {
- defaultMessage = '{{field}} is not a valid day.';
-
- check(value) {
- if (!value) {
- return true;
- }
- if (typeof value !== 'string') {
- return false;
- }
- const [DAY, MONTH, YEAR] = this.component.dayFirst ? [0, 1, 2] : [1, 0, 2];
- const values = value.split('/').map(x => parseInt(x, 10)),
- day = values[DAY],
- month = values[MONTH],
- year = values[YEAR],
- maxDay = getDaysInMonthCount(month, year);
-
- if (isNaN(day) || day < 0 || day > maxDay) {
- return false;
- }
- if (isNaN(month) || month < 0 || month > 12) {
- return false;
- }
- if (isNaN(year) || year < 0 || year > 9999) {
- return false;
- }
- return true;
-
- function isLeapYear(year) {
- // Year is leap if it is evenly divisible by 400 or evenly divisible by 4 and not evenly divisible by 100.
- return !(year % 400) || (!!(year % 100) && !(year % 4));
- }
-
- function getDaysInMonthCount(month, year) {
- switch (month) {
- case 1: // January
- case 3: // March
- case 5: // May
- case 7: // July
- case 8: // August
- case 10: // October
- case 12: // December
- return 31;
- case 4: // April
- case 6: // June
- case 9: // September
- case 11: // November
- return 30;
- case 2: // February
- return isLeapYear(year) ? 29 : 28;
- default:
- return 31;
- }
- }
- }
-}
diff --git a/src/validator/rules/Email.js b/src/validator/rules/Email.js
deleted file mode 100644
index 5fdbb8d88d..0000000000
--- a/src/validator/rules/Email.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import Rule from './Rule';
-
-export default class Email extends Rule {
- defaultMessage = '{{field}} must be a valid email.';
-
- check(value) {
- if (!value) {
- return true;
- }
-
- /* eslint-disable max-len */
- // From http://stackoverflow.com/questions/46155/validate-email-address-in-javascript
- const re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
- /* eslint-enable max-len */
-
- // Allow emails to be valid if the component is pristine and no value is provided.
- return re.test(value);
- }
-}
diff --git a/src/validator/rules/JSON.js b/src/validator/rules/JSON.js
deleted file mode 100644
index 1d6a5c6f07..0000000000
--- a/src/validator/rules/JSON.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import Rule from './Rule';
-
-export default class JSON extends Rule {
- defaultMessage = '{{error}}';
-
- check(value, data, row, index) {
- const { json } = this.settings;
-
- if (!json) {
- return true;
- }
-
- const valid = this.component.evaluate(json, {
- data,
- row,
- rowIndex: index,
- input: value
- });
-
- if (valid === null) {
- return true;
- }
-
- return valid;
- }
-}
diff --git a/src/validator/rules/Mask.js b/src/validator/rules/Mask.js
deleted file mode 100644
index 5cb4a38820..0000000000
--- a/src/validator/rules/Mask.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import { getInputMask, matchInputMask } from '../../utils/utils';
-
-import Rule from './Rule';
-
-export default class Mask extends Rule {
- defaultMessage = '{{field}} does not match the mask.';
-
- check(value) {
- let inputMask;
- if (this.component.isMultipleMasksField) {
- const maskName = value ? value.maskName : undefined;
- const formioInputMask = this.component.getMaskByName(maskName);
- if (formioInputMask) {
- inputMask = getInputMask(formioInputMask);
- }
- value = value ? value.value : value;
- }
- else {
- inputMask = getInputMask(this.settings.mask);
- }
- if (value && inputMask) {
- return matchInputMask(value, inputMask);
- }
- return true;
- }
-}
diff --git a/src/validator/rules/Max.js b/src/validator/rules/Max.js
deleted file mode 100644
index 3d8f4e3e64..0000000000
--- a/src/validator/rules/Max.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import Rule from './Rule';
-
-export default class Max extends Rule {
- defaultMessage = '{{field}} cannot be greater than {{settings.limit}}.';
-
- check(value) {
- const max = parseFloat(this.settings.limit);
- const parsedValue = parseFloat(value);
-
- if (Number.isNaN(max) || Number.isNaN(parsedValue)) {
- return true;
- }
-
- return parsedValue <= max;
- }
-}
diff --git a/src/validator/rules/MaxDate.js b/src/validator/rules/MaxDate.js
deleted file mode 100644
index a4911f851e..0000000000
--- a/src/validator/rules/MaxDate.js
+++ /dev/null
@@ -1,32 +0,0 @@
-import { getDateSetting } from '../../utils/utils';
-import moment from 'moment';
-import _ from 'lodash';
-
-import Rule from './Rule';
-
-export default class MaxDate extends Rule {
- defaultMessage = '{{field}} should not contain date after {{settings.dateLimit}}';
-
- check(value) {
- if (!value) {
- return true;
- }
-
- // If they are the exact same string or object, then return true.
- if (value === this.settings.dateLimit) {
- return true;
- }
-
- const date = moment(value);
- const maxDate = getDateSetting(this.settings.dateLimit);
-
- if (_.isNull(maxDate)) {
- return true;
- }
- else {
- maxDate.setHours(0, 0, 0, 0);
- }
-
- return date.isBefore(maxDate) || date.isSame(maxDate);
- }
-}
diff --git a/src/validator/rules/MaxLength.js b/src/validator/rules/MaxLength.js
deleted file mode 100644
index 47bf1c760f..0000000000
--- a/src/validator/rules/MaxLength.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import Rule from './Rule';
-
-export default class MaxLength extends Rule {
- defaultMessage = '{{field}} must have no more than {{- settings.length}} characters.';
-
- check(value) {
- const maxLength = parseInt(this.settings.length, 10);
- if (!value || !maxLength || !value.hasOwnProperty('length')) {
- return true;
- }
- return (value.length <= maxLength);
- }
-}
diff --git a/src/validator/rules/MaxWords.js b/src/validator/rules/MaxWords.js
deleted file mode 100644
index 25218a525c..0000000000
--- a/src/validator/rules/MaxWords.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import Rule from './Rule';
-
-export default class MaxWords extends Rule {
- defaultMessage = '{{field}} must have no more than {{- settings.length}} words.';
-
- check(value) {
- const maxWords = parseInt(this.settings.length, 10);
- if (!maxWords || (typeof value !== 'string')) {
- return true;
- }
- return (value.trim().split(/\s+/).length <= maxWords);
- }
-}
diff --git a/src/validator/rules/MaxYear.js b/src/validator/rules/MaxYear.js
deleted file mode 100644
index 76a4736026..0000000000
--- a/src/validator/rules/MaxYear.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import Rule from './Rule';
-
-export default class MaxYear extends Rule {
- defaultMessage = '{{field}} should not contain year greater than {{maxYear}}';
-
- check(value) {
- const maxYear = this.settings;
- let year = /\d{4}$/.exec(value);
- year = year ? year[0] : null;
-
- if (!(+maxYear) || !(+year)) {
- return true;
- }
-
- return +year <= +maxYear;
- }
-}
diff --git a/src/validator/rules/Min.js b/src/validator/rules/Min.js
deleted file mode 100644
index 290f41f81e..0000000000
--- a/src/validator/rules/Min.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import Rule from './Rule';
-
-export default class Min extends Rule {
- defaultMessage = '{{field}} cannot be less than {{settings.limit}}.';
-
- check(value) {
- const min = parseFloat(this.settings.limit);
- const parsedValue = parseFloat(value);
-
- if (Number.isNaN(min) || Number.isNaN(parsedValue)) {
- return true;
- }
-
- return parsedValue >= min;
- }
-}
diff --git a/src/validator/rules/MinDate.js b/src/validator/rules/MinDate.js
deleted file mode 100644
index dd50bf9d65..0000000000
--- a/src/validator/rules/MinDate.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import { getDateSetting } from '../../utils/utils';
-import moment from 'moment';
-import _ from 'lodash';
-
-import Rule from './Rule';
-
-export default class MinDate extends Rule {
- defaultMessage = '{{field}} should not contain date before {{settings.dateLimit}}';
-
- check(value) {
- if (!value) {
- return true;
- }
-
- const date = moment(value);
- const minDate = getDateSetting(this.settings.dateLimit);
-
- if (_.isNull(minDate)) {
- return true;
- }
- else {
- minDate.setHours(0, 0, 0, 0);
- }
-
- return date.isAfter(minDate) || date.isSame(minDate);
- }
-}
diff --git a/src/validator/rules/MinLength.js b/src/validator/rules/MinLength.js
deleted file mode 100644
index 7821f5437a..0000000000
--- a/src/validator/rules/MinLength.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import Rule from './Rule';
-
-export default class MinLength extends Rule {
- defaultMessage = '{{field}} must have no more than {{- settings.length}} characters.';
-
- check(value) {
- const minLength = parseInt(this.settings.length, 10);
- if (!minLength || !value || !value.hasOwnProperty('length') || this.component.isEmpty(value)) {
- return true;
- }
- return (value.length >= minLength);
- }
-}
diff --git a/src/validator/rules/MinWords.js b/src/validator/rules/MinWords.js
deleted file mode 100644
index 0b819f4938..0000000000
--- a/src/validator/rules/MinWords.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import Rule from './Rule';
-
-export default class MinWords extends Rule {
- defaultMessage = '{{field}} must have at least {{- settings.length}} words.';
-
- check(value) {
- const minWords = parseInt(this.settings.length, 10);
- if (!minWords || !value || (typeof value !== 'string')) {
- return true;
- }
- return (value.trim().split(/\s+/).length >= minWords);
- }
-}
diff --git a/src/validator/rules/MinYear.js b/src/validator/rules/MinYear.js
deleted file mode 100644
index 2af2a06268..0000000000
--- a/src/validator/rules/MinYear.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import Rule from './Rule';
-
-export default class MinYear extends Rule {
- defaultMessage = '{{field}} should not contain year less than {{minYear}}';
-
- check(value) {
- const minYear = this.settings;
- let year = /\d{4}$/.exec(value);
- year = year ? year[0] : null;
-
- if (!(+minYear) || !(+year)) {
- return true;
- }
-
- return +year >= +minYear;
- }
-}
diff --git a/src/validator/rules/Pattern.js b/src/validator/rules/Pattern.js
deleted file mode 100644
index 54e930720a..0000000000
--- a/src/validator/rules/Pattern.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import Rule from './Rule';
-
-export default class Pattern extends Rule {
- defaultMessage = '{{field}} does not match the pattern {{settings.pattern}}';
-
- check(value) {
- const { pattern } = this.settings;
-
- if (!pattern) {
- return true;
- }
-
- return (new RegExp(`^${pattern}$`)).test(value);
- }
-}
diff --git a/src/validator/rules/Required.js b/src/validator/rules/Required.js
deleted file mode 100644
index 5e3f90915e..0000000000
--- a/src/validator/rules/Required.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import Rule from './Rule';
-
-export default class Required extends Rule {
- defaultMessage = '{{field}} is required';
-
- check(value) {
- // TODO: Day, Survey overrides.
-
- return !this.component.isValueHidden() && !this.component.isEmpty(value);
- }
-}
diff --git a/src/validator/rules/Rule.js b/src/validator/rules/Rule.js
deleted file mode 100644
index 4b07f13264..0000000000
--- a/src/validator/rules/Rule.js
+++ /dev/null
@@ -1,11 +0,0 @@
-export default class Rule {
- constructor(component, settings, config) {
- this.component = component;
- this.settings = settings;
- this.config = config;
- }
-
- check() {
-
- }
-}
diff --git a/src/validator/rules/Select.js b/src/validator/rules/Select.js
deleted file mode 100644
index caf38b295b..0000000000
--- a/src/validator/rules/Select.js
+++ /dev/null
@@ -1,107 +0,0 @@
-import { interpolate } from '../../utils/utils';
-import fetchPonyfill from 'fetch-ponyfill';
-const { fetch, Headers, Request } = fetchPonyfill({
- Promise: Promise
-});
-import _ from 'lodash';
-
-import Rule from './Rule';
-
-export default class Select extends Rule {
- defaultMessage = '{{field}} contains an invalid selection';
-
- check(value, data, row, async) {
- // Skip if value is empty
- if (!value || _.isEmpty(value)) {
- return true;
- }
-
- // Skip if we're not async-capable
- if (!async) {
- return true;
- }
-
- const schema = this.component.component;
-
- // Initialize the request options
- const requestOptions = {
- url: this.settings.url,
- method: 'GET',
- qs: {},
- json: true,
- headers: {}
- };
-
- // If the url is a boolean value
- if (_.isBoolean(requestOptions.url)) {
- requestOptions.url = !!requestOptions.url;
-
- if (
- !requestOptions.url ||
- schema.dataSrc !== 'url' ||
- !schema.data.url ||
- !schema.searchField
- ) {
- return true;
- }
-
- // Get the validation url
- requestOptions.url = schema.data.url;
-
- // Add the search field
- requestOptions.qs[schema.searchField] = value;
-
- // Add the filters
- if (schema.filter) {
- requestOptions.url += (!requestOptions.url.includes('?') ? '?' : '&') + schema.filter;
- }
-
- // If they only wish to return certain fields.
- if (schema.selectFields) {
- requestOptions.qs.select = schema.selectFields;
- }
- }
-
- if (!requestOptions.url) {
- return true;
- }
-
- // Make sure to interpolate.
- requestOptions.url = interpolate(requestOptions.url, { data: this.component.data });
-
- // Add query string to URL
- requestOptions.url += (requestOptions.url.includes('?') ? '&' : '?') + _.chain(requestOptions.qs)
- .map((val, key) => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`)
- .join('&')
- .value();
-
- // Set custom headers.
- if (schema.data && schema.data.headers) {
- _.each(schema.data.headers, header => {
- if (header.key) {
- requestOptions.headers[header.key] = header.value;
- }
- });
- }
-
- // Set form.io authentication.
- if (schema.authenticate && this.config.token) {
- requestOptions.headers['x-jwt-token'] = this.config.token;
- }
-
- return fetch(new Request(requestOptions.url, {
- headers: new Headers(requestOptions.headers)
- }))
- .then(response => {
- if (!response.ok) {
- return false;
- }
-
- return response.json();
- })
- .then((results) => {
- return results && results.length;
- })
- .catch(() => false);
- }
-}
diff --git a/src/validator/rules/Time.js b/src/validator/rules/Time.js
deleted file mode 100644
index 3d35636760..0000000000
--- a/src/validator/rules/Time.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import Rule from './Rule';
-import moment from 'moment';
-
-export default class Time extends Rule {
- defaultMessage = '{{field}} must contain valid time';
-
- check(value) {
- if (this.component.isEmpty(value)) return true;
- return moment(value, this.component.component.format).isValid();
- }
-}
diff --git a/src/validator/rules/Unique.js b/src/validator/rules/Unique.js
deleted file mode 100644
index 3747fd9556..0000000000
--- a/src/validator/rules/Unique.js
+++ /dev/null
@@ -1,70 +0,0 @@
-import { escapeRegExCharacters } from '../../utils/utils';
-import _ from 'lodash';
-import Rule from './Rule';
-
-export default class Unique extends Rule {
- defaultMessage = '{{field}} must be unique';
-
- check(value) {
- // Skip if value is empty object or falsy
- if (!value || _.isObjectLike(value) && _.isEmpty(value)) {
- return true;
- }
-
- // Skip if we don't have a database connection
- if (!this.config.db) {
- return true;
- }
-
- return new Promise(resolve => {
- const form = this.config.form;
- const submission = this.config.submission;
- const path = `data.${this.component.path}`;
-
- // Build the query
- const query = { form: form._id };
-
- if (_.isString(value)) {
- query[path] = {
- $regex: new RegExp(`^${escapeRegExCharacters(value)}$`),
- $options: 'i'
- };
- }
- else if (
- _.isPlainObject(value) &&
- value.address &&
- value.address['address_components'] &&
- value.address['place_id']
- ) {
- query[`${path}.address.place_id`] = {
- $regex: new RegExp(`^${escapeRegExCharacters(value.address['place_id'])}$`),
- $options: 'i'
- };
- }
- // Compare the contents of arrays vs the order.
- else if (_.isArray(value)) {
- query[path] = { $all: value };
- }
- else if (_.isObject(value) || _.isNumber(value)) {
- query[path] = { $eq: value };
- }
-
- // Only search for non-deleted items
- query.deleted = { $eq: null };
-
- // Try to find an existing value within the form
- this.config.db.findOne(query, (err, result) => {
- if (err) {
- return resolve(false);
- }
- else if (result) {
- // Only OK if it matches the current submission
- return resolve(submission._id && (result._id.toString() === submission._id));
- }
- else {
- return resolve(true);
- }
- });
- }).catch(() => false);
- }
-}
diff --git a/src/validator/rules/Url.js b/src/validator/rules/Url.js
deleted file mode 100644
index c458a1ca28..0000000000
--- a/src/validator/rules/Url.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import Rule from './Rule';
-
-export default class Url extends Rule {
- defaultMessage = '{{field}} must be a valid url.';
-
- check(value) {
- /* eslint-disable max-len */
- // From https://stackoverflow.com/questions/8667070/javascript-regular-expression-to-validate-url
- const re = /^(?:(?:(?:https?|ftp):)?\/\/)?(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:[/?#]\S*)?$/i;
- // From http://stackoverflow.com/questions/46155/validate-email-address-in-javascript
- const emailRe = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
- /* eslint-enable max-len */
-
- // Allow urls to be valid if the component is pristine and no value is provided.
- return !value || (re.test(value) && !emailRe.test(value));
- }
-}
diff --git a/src/validator/rules/index.js b/src/validator/rules/index.js
deleted file mode 100644
index e4da2af9c9..0000000000
--- a/src/validator/rules/index.js
+++ /dev/null
@@ -1,46 +0,0 @@
-import custom from './Custom';
-import date from './Date';
-import day from './Day';
-import email from './Email';
-import json from './JSON';
-import mask from './Mask';
-import max from './Max';
-import maxDate from './MaxDate';
-import maxLength from './MaxLength';
-import maxWords from './MaxWords';
-import min from './Min';
-import minDate from './MinDate';
-import minLength from './MinLength';
-import minWords from './MinWords';
-import pattern from './Pattern';
-import required from './Required';
-import select from './Select';
-import unique from './Unique';
-import url from './Url';
-import minYear from './MinYear';
-import maxYear from './MaxYear';
-import time from './Time';
-export default {
- custom,
- date,
- day,
- email,
- json,
- mask,
- max,
- maxDate,
- maxLength,
- maxWords,
- min,
- minDate,
- minLength,
- minWords,
- pattern,
- required,
- select,
- unique,
- url,
- minYear,
- maxYear,
- time,
-};
diff --git a/src/validator/transformers/index.js b/src/validator/transformers/index.js
deleted file mode 100644
index a326363754..0000000000
--- a/src/validator/transformers/index.js
+++ /dev/null
@@ -1,19 +0,0 @@
-export default class Transformers {
- static transformers = {};
-
- static addTransformer(name, transformer) {
- Transformers.transformers[name] = transformer;
- }
-
- static addTransformers(transformers) {
- Transformers.transformers = { ...Transformers.transformers, ...transformers };
- }
-
- static getTransformer(name) {
- return Transformers.transformers[name];
- }
-
- static getTransformers() {
- return Transformers.transformers;
- }
-}
diff --git a/src/validator/valueSources/index.js b/src/validator/valueSources/index.js
deleted file mode 100644
index 4ffac40a5c..0000000000
--- a/src/validator/valueSources/index.js
+++ /dev/null
@@ -1,19 +0,0 @@
-export default class ValueSources {
- static valueSources = {};
-
- static addValueSource(name, valueSource) {
- ValueSources.valueSources[name] = valueSource;
- }
-
- static addValueSources(valueSources) {
- ValueSources.valueSources = { ...ValueSources.valueSources, ...valueSources };
- }
-
- static getValueSource(name) {
- return ValueSources.valueSources[name];
- }
-
- static getValueSources() {
- return ValueSources.valueSources;
- }
-}
diff --git a/src/widgets/CalendarWidget.js b/src/widgets/CalendarWidget.js
index 417c500885..9113387b3c 100644
--- a/src/widgets/CalendarWidget.js
+++ b/src/widgets/CalendarWidget.js
@@ -402,7 +402,8 @@ export default class CalendarWidget extends InputWidget {
}
}
- validationValue(value) {
+ get validationValue() {
+ const value = this.dataValue;
if (typeof value === 'string') {
return new Date(value);
}
@@ -424,7 +425,7 @@ export default class CalendarWidget extends InputWidget {
initFlatpickr(Flatpickr) {
// Create a new flatpickr.
this.calendar = new Flatpickr(this._input, { ...this.settings, disableMobile: true });
- this.calendar.altInput.addEventListener('input', (event) => {
+ this.addEventListener(this.calendar.altInput, 'input', (event) => {
if (this.settings.allowInput && this.settings.currentValue !== event.target.value) {
this.settings.manualInputValue = event.target.value;
this.settings.isManuallyOverriddenValue = true;
@@ -498,6 +499,15 @@ export default class CalendarWidget extends InputWidget {
}
});
+ // If other fields are used to calculate disabled dates, we need to redraw calendar to refresh disabled dates
+ if (this.settings.disableFunction && this.componentInstance && this.componentInstance.root) {
+ this.componentInstance.root.on('change', (e) => {
+ if (e.changed && this.calendar) {
+ this.calendar.redraw();
+ }
+ });
+ }
+
// Restore the calendar value from the component value.
this.setValue(this.componentValue);
}
diff --git a/src/widgets/InputWidget.js b/src/widgets/InputWidget.js
index 196dd70f3d..3bce6ee723 100644
--- a/src/widgets/InputWidget.js
+++ b/src/widgets/InputWidget.js
@@ -46,8 +46,8 @@ export default class InputWidget extends Element {
return value;
}
- validationValue(value) {
- return value;
+ get validationValue() {
+ return this.dataValue;
}
addPrefix() {
diff --git a/test/forms/actions.js b/test/forms/actions.js
index aea1153403..db261b9914 100644
--- a/test/forms/actions.js
+++ b/test/forms/actions.js
@@ -316,7 +316,7 @@ export default {
placeholder: 'Trigger action on method(s)',
dataSrc: 'json',
data: {
- json: '[{"name":"create","title":"Create"},{"name":"update","title":"Update"},{"name":"read","title":"Read"},{"name":"delete","title":"Delete"},{"name":"index","title:"Index"}]'
+ json: '[{"name":"create","title":"Create"},{"name":"update","title":"Update"},{"name":"read","title":"Read"},{"name":"delete","title":"Delete"},{"name":"index","title":"Index"}]'
},
template: ' {{ item.title }}',
valueProperty: 'name',
@@ -799,7 +799,7 @@ export default {
condition: {},
settings: {}
}
- }
+ };
assert.deepEqual(formSubmission, {
data: {
priority: 0,
@@ -853,7 +853,7 @@ export default {
machineName: 'ozvjjccvueotocl:webhooks:webhook'
}
});
-
+
form.destroy();
done();
});
diff --git a/test/forms/editGridOpenWhenEmpty.js b/test/forms/editGridOpenWhenEmpty.js
new file mode 100644
index 0000000000..731e3a5bbb
--- /dev/null
+++ b/test/forms/editGridOpenWhenEmpty.js
@@ -0,0 +1,67 @@
+export default {
+ _id: '65b119c4414257179284c2b4',
+ type: 'form',
+ tags: [],
+ owner: '5e05a6b7549cdc2ece30c6b0',
+ components: [
+ {
+ label: 'Edit Grid',
+ openWhenEmpty: true,
+ tableView: false,
+ rowDrafts: false,
+ key: 'editGrid',
+ type: 'editgrid',
+ input: true,
+ components: [
+ {
+ label: 'Text Field',
+ applyMaskOn: 'change',
+ tableView: true,
+ validate: { required: true },
+ key: 'textField',
+ type: 'textfield',
+ input: true
+ },
+ {
+ label: 'Text Area',
+ applyMaskOn: 'change',
+ autoExpand: false,
+ tableView: true,
+ key: 'textArea',
+ type: 'textarea',
+ input: true
+ }
+ ]
+ },
+ {
+ type: 'button',
+ label: 'Submit',
+ key: 'submit',
+ input: true,
+ tableView: false,
+ showValidations: false,
+ }
+ ],
+ controller: '',
+ revisions: '',
+ _vid: 0,
+ title: 'Edit Grid',
+ display: 'form',
+ access: [
+ {
+ roles: [
+ '5e96e79ee1c3ad3178454100',
+ '5e96e79ee1c3ad3178454101',
+ '5e96e79ee1c3ad3178454102'
+ ],
+ type: 'read_all'
+ }
+ ],
+ submissionAccess: [],
+ settings: {},
+ properties: {},
+ name: 'editGrid',
+ path: 'editgrid',
+};
+
+
\ No newline at end of file
diff --git a/test/forms/formWIthNestedWizard.js b/test/forms/formWIthNestedWizard.js
index 9378218eed..85f4fb65c8 100644
--- a/test/forms/formWIthNestedWizard.js
+++ b/test/forms/formWIthNestedWizard.js
@@ -100,5 +100,3 @@ export default {
name: 'withNestedWizard',
path: 'withnestedwizard'
};
-
-
diff --git a/test/forms/formWithNotAllowedTags.js b/test/forms/formWithNotAllowedTags.js
new file mode 100644
index 0000000000..8ed659091a
--- /dev/null
+++ b/test/forms/formWithNotAllowedTags.js
@@ -0,0 +1,45 @@
+export default {
+ _id: '65bb9a72798447442979fe59',
+ title: 'check dompurify',
+ name: 'checkDompurify',
+ path: 'checkdompurify',
+ type: 'form',
+ display: 'form',
+ components: [
+ {
+ label: 'Text Field with script ',
+ applyMaskOn: 'change',
+ tableView: true,
+ validate: {
+ required: true,
+ },
+ key: 'textFieldWithScript',
+ type: 'textfield',
+ validateWhenHidden: false,
+ input: true,
+ },
+ {
+ label: 'Text Area with iframe ',
+ applyMaskOn: 'change',
+ autoExpand: false,
+ tableView: true,
+ validate: {
+ minLength: 555,
+ },
+ key: 'textAreaWithIframe',
+ type: 'textarea',
+ input: true,
+ },
+ {
+ label: 'Submit',
+ showValidations: false,
+ tableView: false,
+ key: 'submit',
+ type: 'button',
+ input: true,
+ saveOnEnter: false,
+ },
+ ],
+ settings: {},
+ globalSettings: {},
+};
\ No newline at end of file
diff --git a/test/forms/formWithValidateWhenHidden.js b/test/forms/formWithValidateWhenHidden.js
new file mode 100644
index 0000000000..37ae061d35
--- /dev/null
+++ b/test/forms/formWithValidateWhenHidden.js
@@ -0,0 +1,115 @@
+export default {
+ title: 'form with validation',
+ name: 'formWithValidation',
+ path: 'formwithvalidation',
+ type: 'form',
+ display: 'form',
+ components: [
+ {
+ label: 'Number 1',
+ applyMaskOn: 'change',
+ mask: false,
+ tableView: false,
+ delimiter: false,
+ requireDecimal: false,
+ inputFormat: 'plain',
+ truncateMultipleSpaces: false,
+ validateWhenHidden: false,
+ key: 'number1',
+ type: 'number',
+ input: true,
+ },
+ {
+ label: 'Number 2',
+ applyMaskOn: 'change',
+ mask: false,
+ tableView: false,
+ delimiter: false,
+ requireDecimal: false,
+ inputFormat: 'plain',
+ truncateMultipleSpaces: false,
+ validateWhenHidden: false,
+ key: 'number2',
+ type: 'number',
+ input: true,
+ },
+ {
+ label: 'Number',
+ applyMaskOn: 'change',
+ hidden: true,
+ mask: false,
+ tableView: false,
+ delimiter: false,
+ requireDecimal: false,
+ inputFormat: 'plain',
+ truncateMultipleSpaces: false,
+ clearOnHide: false,
+ calculateValue: 'value = data.number1 + data.number2',
+ validate: {
+ customMessage: 'The sum of number 1 and number 2 must not exceed 10.',
+ max: 10,
+ },
+ validateWhenHidden: true,
+ key: 'number',
+ type: 'number',
+ input: true,
+ },
+ {
+ label: 'Text Field',
+ applyMaskOn: 'change',
+ tableView: true,
+ validateWhenHidden: false,
+ key: 'textField',
+ type: 'textfield',
+ input: true,
+ },
+ {
+ label: 'Checkbox',
+ tableView: false,
+ validateWhenHidden: false,
+ key: 'checkbox',
+ conditional: {
+ show: false,
+ conjunction: 'all',
+ },
+ type: 'checkbox',
+ input: true,
+ defaultValue: false,
+ },
+ {
+ label: 'Text Area',
+ applyMaskOn: 'change',
+ autoExpand: false,
+ tableView: true,
+ clearOnHide: false,
+ calculateValue: 'value = data.textField;',
+ validate: {
+ minWords: 3,
+ },
+ validateWhenHidden: true,
+ key: 'textArea',
+ conditional: {
+ show: false,
+ conjunction: 'all',
+ conditions: [
+ {
+ component: 'checkbox',
+ operator: 'isEqual',
+ value: true,
+ },
+ ],
+ },
+ type: 'textarea',
+ input: true,
+ },
+ {
+ label: 'Submit',
+ showValidations: false,
+ tableView: false,
+ key: 'submit',
+ type: 'button',
+ input: true,
+ saveOnEnter: false,
+ },
+ ],
+};
diff --git a/test/forms/formsWithNewSimpleConditions.js b/test/forms/formsWithNewSimpleConditions.js
index dee43a8691..c22a282d34 100644
--- a/test/forms/formsWithNewSimpleConditions.js
+++ b/test/forms/formsWithNewSimpleConditions.js
@@ -1022,11 +1022,53 @@ const form6 = {
machineName: 'cpxkpoxmfvhivle:selectBoxesCond',
};
+const form7 = {
+ type: 'form',
+ display: 'form',
+ components: [
+ {
+ label: 'Checkbox',
+ tableView: false,
+ key: 'checkbox',
+ type: 'checkbox',
+ input: true,
+ },
+ {
+ label: 'Text Field',
+ applyMaskOn: 'change',
+ tableView: true,
+ key: 'textField',
+ conditional: {
+ show: true,
+ conjunction: 'all',
+ conditions: [
+ {
+ component: 'checkbox',
+ operator: 'isEqual',
+ value: 'false',
+ },
+ ],
+ },
+ type: 'textfield',
+ input: true,
+ },
+ {
+ type: 'button',
+ label: 'Submit',
+ key: 'submit',
+ disableOnInvalid: true,
+ input: true,
+ tableView: false,
+ },
+ ],
+};
+
export default {
form1,
form2,
form3,
form4,
form5,
- form6
+ form6,
+ form7
};
diff --git a/test/forms/helpers/testBasicComponentSettings/tests.js b/test/forms/helpers/testBasicComponentSettings/tests.js
index d3acf2b0aa..abc7874bb5 100644
--- a/test/forms/helpers/testBasicComponentSettings/tests.js
+++ b/test/forms/helpers/testBasicComponentSettings/tests.js
@@ -1,5 +1,7 @@
import assert from 'power-assert';
import _ from 'lodash';
+import FormioUtils from '../../../../src/utils';
+
import settings from './settings';
import values from './values';
@@ -186,7 +188,6 @@ export default {
const componentKey = component.component.key;
if (child && componentType === 'datagrid') return; //BUG: remove the check once it is fixed;
-
const disabled = _.isBoolean(component.disabled) ? component.disabled : component._disabled;
assert.equal(
@@ -570,35 +571,35 @@ export default {
});
});
},
- 'Should highlight modal button if component is invalid'(form, done, test) {
- test.timeout(10000);
- const testComponents = form.components.filter(comp => !['htmlelement', 'content', 'button'].includes(comp.component.type));
-
- form.everyComponent((comp) => {
- comp.component.validate = comp.component.validate || {};
- comp.component.validate.required = true;
- });
- setTimeout(() => {
- const clickEvent = new Event('click');
- form.getComponent('submit').refs.button.dispatchEvent(clickEvent);
- setTimeout(() => {
- testComponents
- .filter(comp => !comp.component.tree && comp.hasInput)
- .forEach((comp) => {
- const compKey = comp.component.key;
- const compType = comp.component.type;
-
- const isErrorHighlightClass = !!(comp.refs.openModalWrapper.classList.contains('formio-error-wrapper') || comp.componentModal.element.classList.contains('formio-error-wrapper'));
- assert.deepEqual(comp.subForm ? !!comp.subForm.errors.length : !!comp.error, true, `${compKey} (component ${compType}): should contain validation error`);
- //BUG in nested forms, remove the check once it is fixed
- if (compType !== 'form') {
- assert.deepEqual(isErrorHighlightClass, true, `${compKey} (component ${compType}): should highlight invalid modal button`);
- }
- });
- done();
- }, 200);
- }, 200);
- },
+ // 'Should highlight modal button if component is invalid'(form, done, test) {
+ // test.timeout(10000);
+ // const testComponents = form.components.filter(comp => !['htmlelement', 'content', 'button'].includes(comp.component.type));
+
+ // form.everyComponent((comp) => {
+ // comp.component.validate = comp.component.validate || {};
+ // comp.component.validate.required = true;
+ // });
+ // setTimeout(() => {
+ // const clickEvent = new Event('click');
+ // form.getComponent('submit').refs.button.dispatchEvent(clickEvent);
+ // setTimeout(() => {
+ // testComponents
+ // .filter(comp => !comp.component.tree && comp.hasInput)
+ // .forEach((comp) => {
+ // const compKey = comp.component.key;
+ // const compType = comp.component.type;
+
+ // const isErrorHighlightClass = !!(comp.refs.openModalWrapper.classList.contains('formio-error-wrapper') || comp.componentModal.element.classList.contains('formio-error-wrapper'));
+ // assert.deepEqual(comp.subForm ? !!comp.subForm.errors.length : !!comp.errors.length, 1, `${compKey} (component ${compType}): should contain validation error`);
+ // //BUG in nested forms, remove the check once it is fixed
+ // if (compType !== 'form') {
+ // assert.deepEqual(isErrorHighlightClass, true, `${compKey} (component ${compType}): should highlight invalid modal button`);
+ // }
+ // });
+ // done();
+ // }, 200);
+ // }, 200);
+ // },
},
calculateValue: {
'Should caclulate component value'(form, done, test) {
@@ -611,7 +612,7 @@ export default {
form.components.forEach(comp => {
const compKey = comp.component.key;
const compType = comp.component.type;
- if (compKey === 'basis') return;
+ if (compKey === 'basis' || compType === 'button') return;
const getExpectedCalculatedValue = (basis) => settings.calculateValue[`${compKey}`].expectedValue(basis);
@@ -745,12 +746,10 @@ export default {
const getExpectedErrorMessage = () => `${comp.component.label} is required`;
- assert.deepEqual(!!comp.error, true, `${compKey} (component ${compType}): should have required validation error`);
- assert.deepEqual(comp.error.message, getExpectedErrorMessage(), `${compKey} (component ${compType}): should have correct rquired validation message`);
+ assert.deepEqual(comp.visibleErrors.length, 1, `${compKey} (component ${compType}): should have required validation error`);
+ assert.deepEqual(comp.errors[0].message, getExpectedErrorMessage(), `${compKey} (component ${compType}): should have correct rquired validation message`);
assert.deepEqual(comp.pristine, false, `${compKey} (component ${compType}): should set pristine to false`);
assert.deepEqual(comp.element.classList.contains('formio-error-wrapper'), true, `${compKey} (component ${compType}): should set error class`);
- //remove below line once tree validation error display is fixed
- if (_.includes(['tree'], comp.component.type)) return;
assert.deepEqual(comp.refs.messageContainer.querySelector('.error')?.textContent.trim(), getExpectedErrorMessage(), `${compKey} (component ${compType}): should display error message`);
});
@@ -765,12 +764,12 @@ export default {
const compType = comp.component.type;
assert.deepEqual(comp.dataValue, _.get(values.values, compKey), `${compKey} (component ${compType}): should set value`);
- assert.deepEqual(!!comp.error, false, `${compKey} (component ${compType}): Should remove error`);
+ assert.deepEqual(comp.visibleErrors.length, 0, `${compKey} (component ${compType}): Should remove error`);
assert.deepEqual(comp.element.classList.contains('formio-error-wrapper'), false, `${compKey} (component ${compType}): Should remove error class`);
assert.deepEqual(!!comp.refs.messageContainer.querySelector('.error'), false, `${compKey} (component ${compType}): should clear errors`);
});
done();
- }, 300);
+ }, 350);
}, 300);
},
'Should show custom validation error if component is invalid'(form, done, test) {
@@ -793,10 +792,8 @@ export default {
const getExpectedErrorMessage = () => `${compKey}: custom validation error`;
- assert.deepEqual(!!comp.error, true, `${compKey} (component ${compType}): should have required validation error`);
- assert.deepEqual(comp.error.message, getExpectedErrorMessage(), `${compKey} (component ${compType}): should have correct custom validation message`);
- //remove below line once tree validation error display is fixed
- if (_.includes(['tree'], comp.component.type)) return;
+ assert.deepEqual(comp.visibleErrors.length, 1, `${compKey} (component ${compType}): should have required validation error`);
+ assert.deepEqual(comp.visibleErrors[0].message, getExpectedErrorMessage(), `${compKey} (component ${compType}): should have correct custom validation message`);
assert.deepEqual(comp.refs.messageContainer.querySelector('.error')?.textContent.trim(), getExpectedErrorMessage(), `${compKey} (component ${compType}): should display custom error message`);
});
done();
@@ -822,10 +819,8 @@ export default {
const getExpectedErrorMessage = () => `Custom label for ${compKey} is required`;
- assert.deepEqual(!!comp.error, true, `${compKey} (component ${compType}): should have required validation error with custom label`);
- assert.deepEqual(comp.error.message, getExpectedErrorMessage(), `${compKey} (component ${compType}): should have correct required validation message with custom label`);
- //remove below line once tree validation error display is fixed
- if (_.includes(['tree'], comp.component.type)) return;
+ assert.deepEqual(comp.visibleErrors.length, 1, `${compKey} (component ${compType}): should have required validation error with custom label`);
+ assert.deepEqual(comp.visibleErrors[0].message, getExpectedErrorMessage(), `${compKey} (component ${compType}): should have correct required validation message with custom label`);
assert.deepEqual(comp.refs.messageContainer.querySelector('.error')?.textContent.trim(), getExpectedErrorMessage(), `${compKey} (component ${compType}): should display error message with custom label`);
});
done();
@@ -835,7 +830,6 @@ export default {
'validate.custom': {
'Should execute custom validation'(form, done, test) {
test.timeout(3000);
- const testComponents = form.components.filter(comp => !['button'].includes(comp.component.type));
assert.deepEqual(form.errors.length, 0, 'Should not show validation errors');
form.setPristine(false);
@@ -844,16 +838,18 @@ export default {
});
setTimeout(() => {
- assert.deepEqual(form.errors.length, testComponents.length, 'Form should contain references to all components errors');
+ // minus one to not include the submit button.
+ assert.deepEqual(form.errors.length, form.components.length - 1, 'Form should contain references to all components errors');
- testComponents.forEach(comp => {
+ form.components.forEach(comp => {
const compKey = comp.component.key;
const compType = comp.component.type;
+ if (compType === 'button') return;
const getExpectedErrorMessage = () => 'Custom validation message: component is invalid.';
assert.deepEqual(comp.dataValue, _.get(values.values, compKey), `${compKey} (component ${compType}): should set value`);
- assert.deepEqual(!!comp.error, true, `${compKey} (component ${compType}): should have validation error`);
- assert.deepEqual(comp.error.message, getExpectedErrorMessage(), `${compKey} (component ${compType}): should have correct rquired validation message`);
+ assert.deepEqual(comp.visibleErrors.length, 1, `${compKey} (component ${compType}): should have validation error`);
+ assert.deepEqual(comp.visibleErrors[0].message, getExpectedErrorMessage(), `${compKey} (component ${compType}): should have correct rquired validation message`);
assert.deepEqual(comp.pristine, false, `${compKey} (component ${compType}): should set pristine to false`);
assert.deepEqual(comp.element.classList.contains('has-error'), true, `${compKey} (component ${compType}): should set error class`);
@@ -867,17 +863,17 @@ export default {
return _.isNumber(comp.dataValue) ? 33333333 : comp.defaultValue;
};
- _.each(testComponents, (comp) => {
+ _.each(form.components, (comp) => {
comp.setValue(getSetValue(comp));
});
setTimeout(() => {
- assert.deepEqual(form.errors.length, 0, 'Should remove validation errors after setting valid values');
- testComponents.forEach(comp => {
+ assert.deepEqual(form.visibleErrors.length, 0, 'Should remove validation errors after setting valid values');
+ form.components.forEach(comp => {
const compKey = comp.component.key;
const compType = comp.component.type;
- assert.deepEqual(!!comp.error, false, `${compKey} (component ${compType}): Should remove validation error`);
+ assert.deepEqual(comp.visibleErrors.length, 0, `${compKey} (component ${compType}): Should remove validation error`);
assert.deepEqual(comp.element.classList.contains('has-error'), false, `${compKey} (component ${compType}): Should remove error class`);
assert.deepEqual(!!comp.refs.messageContainer.querySelector('.error'), false, `${compKey} (component ${compType}): should clear errors list`);
});
@@ -890,15 +886,19 @@ export default {
'Should show validation errors for nested components'(form, done, test) {
test.timeout(6000);
const testComponents = [];
- const treeComponent = form.getComponent('tree');
- form.everyComponent((comp) => {
- const component = comp.component;
- //BUG: exclude datagrid from the check once it required validation issue is fixed
- if (!component.validate_nested_components && ![...layoutComponents, 'datagrid'].includes(component.type) && (!treeComponent || !treeComponent.getComponents().includes(comp))) {
+ FormioUtils.eachComponent(form.component.components, (component) => {
+ const componentInstance = form.getComponent(component.key);
+ if (component.type === 'datagrid') {
+ componentInstance.component.components.forEach((comp) => _.set(comp, 'validate.required', true));
+ }
+ if (!component.validate_nested_components && ![...layoutComponents, 'datagrid', 'tree'].includes(component.type)) {
+ if (componentInstance) {
+ _.set(componentInstance.component, 'validate.required', true);
+ testComponents.push(componentInstance);
+ }
_.set(component, 'validate.required', true);
- testComponents.push(comp);
}
- });
+ }, true);
setTimeout(() => {
const clickEvent = new Event('click');
form.getComponent('submit').refs.button.dispatchEvent(clickEvent);
@@ -913,8 +913,8 @@ export default {
const getExpectedErrorMessage = () => `${comp.component.label} is required`;
- assert.deepEqual(!!comp.error, true, `${compKey} (component ${compType}): should have required validation error`);
- assert.deepEqual(comp.error.message, getExpectedErrorMessage(), `${compKey} (component ${compType}): should have correct rquired validation message`);
+ assert.deepEqual(comp.visibleErrors.length, 1, `${compKey} (component ${compType}): should have required validation error`);
+ assert.deepEqual(comp.visibleErrors[0].message, getExpectedErrorMessage(), `${compKey} (component ${compType}): should have correct rquired validation message`);
assert.deepEqual(comp.pristine, false, `${compKey} (component ${compType}): should set pristine to false`);
assert.deepEqual(comp.element.classList.contains('formio-error-wrapper'), true, `${compKey} (component ${compType}): should set error class`);
@@ -933,20 +933,20 @@ export default {
});
setTimeout(() => {
- assert.deepEqual(form.errors.length, 0, 'Should remove required validation errors after setting values');
+ assert.deepEqual(form.visibleErrors.length, 0, 'Should remove required validation errors after setting values');
testComponents.forEach(comp => {
const compKey = comp.component.key;
const compType = comp.component.type;
- assert.deepEqual(!!comp.error, false, `${compKey} (component ${compType}): Should remove valudation error`);
+ assert.deepEqual(comp.visibleErrors.length, 0, `${compKey} (component ${compType}): Should remove valudation error`);
assert.deepEqual(comp.element.classList.contains('formio-error-wrapper'), false, `${compKey} (component ${compType}): Should remove error class`);
assert.deepEqual(!!comp.refs.messageContainer.querySelector('.error'), false, `${compKey} (component ${compType}): should clear errors`);
});
done();
}, 700);
- }, 300);
- }, 300);
+ }, 700);
+ }, 700);
},
},
conditional: {
diff --git a/test/forms/nestedDataWithModalViewAndLayoutComponents.json b/test/forms/nestedDataWithModalViewAndLayoutComponents.json
new file mode 100644
index 0000000000..14064bda51
--- /dev/null
+++ b/test/forms/nestedDataWithModalViewAndLayoutComponents.json
@@ -0,0 +1,54 @@
+{
+ "name": "fio3703",
+ "path": "fio3703",
+ "type": "form",
+ "display": "form",
+ "components": [
+ {
+ "label": "Data Grid",
+ "reorder": false,
+ "addAnotherPosition": "bottom",
+ "layoutFixed": false,
+ "enableRowGroups": false,
+ "initEmpty": false,
+ "tableView": true,
+ "modalEdit": true,
+ "defaultValue": [
+ {
+ "textField": ""
+ }
+ ],
+ "key": "dataGrid",
+ "type": "datagrid",
+ "input": true,
+ "components": [
+ {
+ "collapsible": false,
+ "key": "panel",
+ "type": "panel",
+ "label": "Panel",
+ "input": false,
+ "tableView": false,
+ "components": [
+ {
+ "label": "Text Field",
+ "applyMaskOn": "change",
+ "tableView": true,
+ "key": "textField",
+ "type": "textfield",
+ "input": true
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "button",
+ "label": "Submit",
+ "key": "submit",
+ "disableOnInvalid": true,
+ "input": true,
+ "tableView": false
+ }
+ ]
+}
diff --git a/test/forms/wizardValidationOnPageChanged.d.ts b/test/forms/wizardValidationOnPageChanged.d.ts
index 2abee6e9d6..37163856ae 100644
--- a/test/forms/wizardValidationOnPageChanged.d.ts
+++ b/test/forms/wizardValidationOnPageChanged.d.ts
@@ -1,5 +1,4 @@
-declare namespace _default {
- const title: string;
+declare namespace _default { const title: string;
const components: ({
title: string;
label: string;
diff --git a/test/formtest/settingErrors.json b/test/formtest/settingErrors.json
index f95dedfe4a..fb7b920dbc 100644
--- a/test/formtest/settingErrors.json
+++ b/test/formtest/settingErrors.json
@@ -1420,7 +1420,7 @@
"description": "",
"map": {
"region": "",
- "key": "AIzaSyBNL2e4MnmyPj9zN7SVAe428nCSLP1X144"
+ "key": ""
},
"tooltip": "",
"customClass": "",
diff --git a/test/harness.js b/test/harness.js
index 463c3115db..a8f8f6e0af 100644
--- a/test/harness.js
+++ b/test/harness.js
@@ -2,7 +2,6 @@ import assert from 'power-assert';
import _ from 'lodash';
import EventEmitter from 'eventemitter3';
import { expect } from 'chai';
-import { I18n } from '../src/utils/i18n';
import FormBuilder from '../src/FormBuilder';
import AllComponents from '../src/components';
import Components from '../src/components/Components';
@@ -11,8 +10,8 @@ Components.setComponents(AllComponents);
if (process) {
// Do not handle unhandled rejections.
- process.on('unhandledRejection', () => {
- console.warn('Unhandled rejection!');
+ process.on('unhandledRejection', (err) => {
+ console.warn('Unhandled rejection:', err?.message || err);
});
}
@@ -137,7 +136,8 @@ const Harness = {
events: new EventEmitter(),
}, options));
component.pristine = false;
- return new Promise((resolve, reject) => {
+ component.componentsMap[component.key] = component;
+ return new Promise((resolve) => {
// Need a parent element to redraw.
const parent = document.createElement('div');
const element = document.createElement('div');
@@ -300,8 +300,10 @@ const Harness = {
testErrors(form, submission, errors, done) {
form.on('error', (err) => {
_.each(errors, (error, index) => {
- error.component = form.getComponent(error.component).component;
- assert.deepEqual(err[index].component, error.component);
+ if (error.component) {
+ error.component = form.getComponent(error.component.key).component;
+ assert.deepEqual(err[index].component, error.component);
+ }
assert.equal(err[index].message, error.message);
});
form.off('error');
@@ -364,7 +366,10 @@ const Harness = {
let testBad = true;
component.on('componentChange', (change) => {
const valid = component.checkValidity();
- if (valid && !testBad) {
+ if (testBad && valid) {
+ return done(new Error('Validation should not pass.'));
+ }
+ if (!testBad && valid) {
assert.equal(change.value, test.good.value);
done();
}
@@ -373,10 +378,12 @@ const Harness = {
if (!testBad) {
return done(new Error('Validation Error'));
}
- testBad = false;
assert.equal(error.component.key, test.bad.field);
assert.equal(error.message, test.bad.error);
- component.setValue(test.good.value);
+ setTimeout(() => {
+ testBad = false;
+ component.setValue(test.good.value);
+ }, 1);
});
// Set the value.
diff --git a/test/renders/component-bootstrap-radio-multiple.html b/test/renders/component-bootstrap-radio-multiple.html
index 6ee85a55db..779e13f2dc 100644
--- a/test/renders/component-bootstrap-radio-multiple.html
+++ b/test/renders/component-bootstrap-radio-multiple.html
@@ -4,8 +4,8 @@
diff --git a/test/renders/component-bootstrap-radio-readOnly.html b/test/renders/component-bootstrap-radio-readOnly.html
index 65716d3318..a65c350097 100644
--- a/test/renders/component-bootstrap-radio-readOnly.html
+++ b/test/renders/component-bootstrap-radio-readOnly.html
@@ -4,8 +4,8 @@
diff --git a/test/renders/component-bootstrap-radio-required.html b/test/renders/component-bootstrap-radio-required.html
index 805885ed44..67e39b752b 100644
--- a/test/renders/component-bootstrap-radio-required.html
+++ b/test/renders/component-bootstrap-radio-required.html
@@ -4,8 +4,8 @@
diff --git a/test/renders/component-bootstrap-radio.html b/test/renders/component-bootstrap-radio.html
index 7277360a9e..986d4990da 100644
--- a/test/renders/component-bootstrap-radio.html
+++ b/test/renders/component-bootstrap-radio.html
@@ -4,8 +4,8 @@
diff --git a/test/renders/component-bootstrap-selectboxes-multiple.html b/test/renders/component-bootstrap-selectboxes-multiple.html
index 71d7951a93..8cb0067d9b 100644
--- a/test/renders/component-bootstrap-selectboxes-multiple.html
+++ b/test/renders/component-bootstrap-selectboxes-multiple.html
@@ -4,8 +4,8 @@
diff --git a/test/renders/component-bootstrap-selectboxes-readOnly.html b/test/renders/component-bootstrap-selectboxes-readOnly.html
index 3584ad9a02..f1d40c8f45 100644
--- a/test/renders/component-bootstrap-selectboxes-readOnly.html
+++ b/test/renders/component-bootstrap-selectboxes-readOnly.html
@@ -4,8 +4,8 @@
diff --git a/test/renders/component-bootstrap-selectboxes-required.html b/test/renders/component-bootstrap-selectboxes-required.html
index eea9f9e079..bfeb6fb11c 100644
--- a/test/renders/component-bootstrap-selectboxes-required.html
+++ b/test/renders/component-bootstrap-selectboxes-required.html
@@ -4,8 +4,8 @@
diff --git a/test/renders/component-bootstrap-selectboxes.html b/test/renders/component-bootstrap-selectboxes.html
index 230b08d4df..da2e53dc35 100644
--- a/test/renders/component-bootstrap-selectboxes.html
+++ b/test/renders/component-bootstrap-selectboxes.html
@@ -4,8 +4,8 @@
diff --git a/tsconfig.cjs.json b/tsconfig.cjs.json
index 80b26c8029..b0d7f918ff 100644
--- a/tsconfig.cjs.json
+++ b/tsconfig.cjs.json
@@ -3,7 +3,6 @@
"compilerOptions": {
"module": "commonjs",
"outDir": "./lib/cjs",
- "target": "es2015",
- "declaration": true
+ "target": "ES2015"
}
}
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
index d1811e9f96..fe89ac154a 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -2,7 +2,7 @@
"compilerOptions": {
"module": "commonjs",
"outDir": "lib",
- "target": "es2015",
+ "target": "ES2015",
"allowJs": true, /* Allow javascript files to be compiled. */
"allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"noImplicitThis": false, /* Raise error on 'this' expressions with an implied 'any' type. */
diff --git a/tsconfig.mjs.json b/tsconfig.mjs.json
index f818f58ff4..96a508e3f2 100644
--- a/tsconfig.mjs.json
+++ b/tsconfig.mjs.json
@@ -3,7 +3,6 @@
"compilerOptions": {
"module": "esnext",
"outDir": "./lib/mjs",
- "target": "esnext",
- "declaration": true
+ "target": "esnext"
}
}
\ No newline at end of file
diff --git a/types/Embed.d.ts b/types/Embed.d.ts
new file mode 100644
index 0000000000..f1ca9af2bb
--- /dev/null
+++ b/types/Embed.d.ts
@@ -0,0 +1,13 @@
+export declare class Formio {
+ static config: any;
+ static icons: string;
+ static setBaseUrl(url: string): void;
+ static setApiUrl(url: string): void;
+ static setAppUrl(url: string): void;
+ static setProjectUrl(url: string): void;
+ static clearCache(): void;
+ static setPathType(type: string): void;
+ static createForm(element: HTMLElement, form: any, options: any): any;
+ static builder(element: HTMLElement, form: any, options: any): any;
+ static use(module: any): void;
+}
diff --git a/types/Plugins.d.ts b/types/Plugins.d.ts
new file mode 100644
index 0000000000..37afe4929f
--- /dev/null
+++ b/types/Plugins.d.ts
@@ -0,0 +1,95 @@
+/**
+ * The plugin initialization function, which will receive the Formio interface as its first argument.
+ */
+export interface PluginInitFunction {
+ /**
+ * @param Formio - The Formio interface class.
+ */
+ (Formio: any): void;
+}
+/**
+ * Function that is called when the plugin is deregistered.
+ */
+export interface PluginDeregisterFunction {
+ /**
+ * @param Formio The Formio interface class.
+ */
+ (Formio: any): void;
+}
+/**
+ * A Formio Plugin interface.
+ */
+export interface Plugin {
+ /**
+ * The name of the plugin.
+ */
+ __name: string;
+ /**
+ * The priority of this plugin.
+ */
+ priority: number;
+ /**
+ * An initialization function called when registered with Formio.
+ */
+ init: PluginInitFunction;
+ /**
+ * Called when the plugin is deregistered.
+ */
+ deregister: PluginDeregisterFunction;
+}
+/**
+ * The Form.io Plugins allow external systems to "hook" into the default behaviors of the JavaScript SDK.
+ */
+export default class Plugins {
+ /**
+ * An array of Form.io Plugins.
+ */
+ static plugins: Array ;
+ /**
+ * The Formio class.
+ */
+ static Formio: any;
+ /**
+ * Returns the plugin identity.
+ *
+ * @param value
+ */
+ static identity(value: string): string;
+ /**
+ * De-registers a plugin.
+ * @param plugin The plugin you wish to deregister.
+ */
+ static deregisterPlugin(plugin: (Plugin | string)): boolean;
+ /**
+ * Registers a new plugin.
+ *
+ * @param plugin The Plugin object.
+ * @param name The name of the plugin you wish to register.
+ */
+ static registerPlugin(plugin: Plugin, name: string): void;
+ /**
+ * Returns a plugin provided the name of the plugin.
+ * @param name The name of the plugin you would like to get.
+ */
+ static getPlugin(name: string): Plugin | null;
+ /**
+ * Wait for a plugin function to complete.
+ * @param pluginFn - A function within the plugin.
+ * @param args
+ */
+ static pluginWait(pluginFn: any, ...args: any[]): Promise;
+ /**
+ * Gets a value from a Plugin
+ * @param pluginFn
+ * @param args
+ */
+ static pluginGet(pluginFn: any, ...args: any[]): any;
+ /**
+ * Allows a Plugin to alter the behavior of the JavaScript library.
+ *
+ * @param pluginFn
+ * @param value
+ * @param args
+ */
+ static pluginAlter(pluginFn: any, value: any, ...args: any[]): any;
+}
diff --git a/types/components/_classes/component/component.d.ts b/types/components/_classes/component/component.d.ts
index 443c94441f..4d8bf89a3a 100644
--- a/types/components/_classes/component/component.d.ts
+++ b/types/components/_classes/component/component.d.ts
@@ -1,5 +1,6 @@
import { Element } from '../../../element';
-import { ComponentSchema, ElementInfo, ExtendedComponentSchema, ValidateOptions } from './../../schema.d';
+import { ValidateOptions } from '../../../formio';
+import { ComponentSchema, ElementInfo, ExtendedComponentSchema } from './../../schema.d';
export class Component extends Element {
static schema(sources: ExtendedComponentSchema): ExtendedComponentSchema;
diff --git a/types/components/schema.d.ts b/types/components/schema.d.ts
index fc3b2e7186..4ebb660309 100644
--- a/types/components/schema.d.ts
+++ b/types/components/schema.d.ts
@@ -1,3 +1,5 @@
+import { ConditionalOptions, ValidateOptions } from '../formio';
+
export interface ComponentSchema {
/**
* The type of component
@@ -154,74 +156,6 @@ export interface ComponentSchema {
export type ExtendedComponentSchema = ComponentSchema & { [key: string]: any };
-export interface ConditionalOptions {
- /** If the field should show if the condition is true */
- show?: boolean;
- /** The field API key that it should compare its value against to determine if the condition is triggered. */
- when?: string;
- /** The value that should be checked against the comparison component */
- eq?: string;
- /** The JSON Logic to determine if this component is conditionally available.
- * Fyi: http://jsonlogic.com/
- */
- json?: Object;
-}
-
-export interface ValidateOptions {
- /**
- * If this component is required.
- */
- required?: boolean;
-
- /**
- * For text input, this checks the minimum length of text for valid input
- */
- minLength?: number;
-
- /**
- * For text input, this checks the maximum length of text for valid input
- */
- maxLength?: number;
-
- /**
- * For text input, this checks the text agains a Regular expression pattern.
- */
- pattern?: string;
-
- /**
- * A custom javascript based validation or a JSON object for using JSON Logic
- */
- custom?: any;
-
- /**
- * If the custom validation should remain private (only the backend will see it and execute it).
- */
- customPrivate?: boolean;
-
- /**
- * Minimum value for numbers
- */
- min?: number;
-
- /**
- * Maximum value for numbers
- */
- max?: number;
-
- minSelectedCount?: number;
- maxSelectedCount?: number;
- minWords?: number;
- maxWords?: number;
- email?: boolean;
- url?: boolean;
- date?: boolean;
- day?: boolean;
- json?: string;
- mask?: boolean;
- minDate?: any;
- maxDate?: any;
-}
-
export interface ElementInfo {
type: string;
component: ExtendedComponentSchema;
diff --git a/types/formio.d.ts b/types/formio.d.ts
index 91266641d9..6368096ac7 100644
--- a/types/formio.d.ts
+++ b/types/formio.d.ts
@@ -1,121 +1,1236 @@
-export class Formio {
- constructor(url: string, options?: Object);
- public base: string;
- public projectsUrl: string;
- public projectUrl: string;
- public projectId: string;
- public formId: string;
- public submissionId: string;
- public actionsUrl: string;
- public actionId: string;
- public actionUrl: string;
- public vsUrl: string;
- public vId: string;
- public vUrl: string;
- public query: string;
- public formUrl?: string;
- public formsUrl?: string;
- public submissionUrl?: string;
- public submissionsUrl?: string;
- public token: any | string;
- static libraries: any;
- static Promise: any;
- static fetch: any;
- static Headers: any;
- static baseUrl: string;
- static projectUrl: string;
- static authUrl: string;
- static projectUrlSet: boolean;
- static plugins: any;
- static cache: any;
- static license: string;
- static providers: any;
- static events: any; // EventEmitter3
- static namespace: string;
- static formOnly?: boolean;
- static rulesEntities: any;
- static options: any;
- delete(type: any, opts?: any): any;
- index(type: any, query?: any, opts?: any): any;
- save(type: any, data: any, opts?: any): any;
- load(type: any, query?: any, opts?: any): any;
- makeRequest(...args: any[]): any;
- loadProject(query?: any, opts?: any): any;
- saveProject(data: any, opts?: any): any;
- deleteProject(opts?: any): any;
- static loadProjects(query?: any, opts?: any): any;
- static createForm(element: any, form: string | Object, options?: Object): Promise;
- static setBaseUrl(url: string): void;
- static setProjectUrl(url: string): void;
- static setAuthUrl(url: string): void;
- static getToken(options?: any): any;
- static makeStaticRequest(url: string, method?: string, data?: any, opts?: Object): any;
- static makeRequest(formio?: Formio, type?: string, url?: string, method?: string, data?: any, opts?: Object): any;
- static currentUser(formio?: Formio, options?: Object): any;
- static logout(formio?: Formio, options?: Object): any;
- static clearCache(): void;
- static setUser(user: any, opts?: Object): any;
- loadForm(query?: any, opts?: Object): any;
- loadForms(query?: any, opts?: Object): any;
- loadSubmission(query?: any, opts?: Object): any;
- loadSubmissions(query?: any, opts?: Object): any;
- userPermissions(
- user?: any,
- form?: any,
- submission?: any,
- ): Promise<{ create: boolean; read: boolean; edit: boolean; delete: boolean }>;
- createform(form: Object): Promise;
- saveForm(data: any, opts?: Object): any;
- saveSubmission(data: any, opts?: Object): any;
- deleteForm(opts?: Object): any;
- deleteSubmission(opts?: Object): any;
- saveAction(data: any, opts?: any): any;
- deleteAction(opts?: any): any;
- loadAction(query?: any, opts?: any): any;
- loadActions(query?: any, opts?: any): any;
- availableActions(): any;
- actionInfo(name: any): any;
- isObjectId(id: any): any;
- getProjectId(): any;
- getFormId(): any;
- currentUser(options?: Object): any;
- accessInfo(): any;
- getToken(options?: Object): any;
- setToken(token: any, options?: Object): any;
- getTempToken(expire: any, allowed: any, options?: Object): any;
- getDownloadUrl(form: any): any;
- uploadFile(storage: any, file: any, fileName: any, dir: any, progressCallback: any, url: any, options?: Object): any;
- downloadFile(file: any, options?: Object): any;
- canSubmit(): any;
- getUrlParts(url: any): any;
- static use(plugin: any): any;
- static getUrlParts(url: any, formio?: Formio): any;
- static serialize(obj: any, _interpolate: any): any;
- static getRequestArgs(formio: Formio, type: string, url: string, method?: string, data?: any, opts?: any): any;
- static request(url: any, method?: any, data?: any, header?: any, opts?: any): any;
- static setToken(token: string, opts?: any): any;
- static getUser(options?: Object): any;
- static getBaseUrl(): string;
- static setApiUrl(url: string): any;
- static getApiUrl(): any;
- static setAppUrl(url: string): any;
- static getAppUrl(): any;
- static getProjectUrl(): any;
- static noop(): any;
- static identity(value: any): any;
- static deregisterPlugin(plugin: any): any;
- static registerPlugin(plugin: any, name: string): any;
- static getPlugin(name: any | string): any;
- static pluginWait(pluginFn: any, ...args: any[]): any;
- static pluginGet(pluginFn: any, ...args: any[]): any;
- static pluginAlter(pluginFn: any, value: any, ...args: any[]): any;
- static accessInfo(formio?: Formio): any;
- static pageQuery(): any;
- static oAuthCurrentUser(formio?: Formio, token?: any): any;
- static samlInit(options?: Object): any;
- static oktaInit(options?: Object): any;
- static ssoInit(type: any, options?: Object): any;
- static requireLibrary(name: any, property: any, src: any, polling?: boolean): any;
- static libraryReady(name: string): any;
- oauthLogoutURI(uri: string, options?: any): any;
+import EventEmitter from 'eventemitter3';
+import Plugins from './Plugins';
+
+export interface FormioLibrarySource {
+ type: string;
+ src: string;
+}
+
+/**
+ * The Formio class options interface.
+ */
+export interface FormioOptions {
+ /**
+ * The base API url of the Form.io Platform. Example: https://api.form.io
+ */
+ base?: string;
+ /**
+ * The project API url of the Form.io Project. Example: https://examples.form.io
+ */
+ project?: string;
+ useSessionToken?: boolean;
+ formOnly?: boolean;
+}
+/**
+ * The different path types for a project.
+ */
+export declare enum FormioPathType {
+ Subdirectories = 'Subdirectories',
+ Subdomains = 'Subdomains'
+}
+
+export interface ConditionalOptions {
+ /** If the field should show if the condition is true */
+ show?: boolean;
+ /** The field API key that it should compare its value against to determine if the condition is triggered. */
+ when?: string;
+ /** The value that should be checked against the comparison component */
+ eq?: string;
+ /** The JSON Logic to determine if this component is conditionally available.
+ * Fyi: http://jsonlogic.com/
+ */
+ json?: Object;
+}
+
+export interface ValidateOptions {
+ /**
+ * If this component is required.
+ */
+ required?: boolean;
+
+ /**
+ * For text input, this checks the minimum length of text for valid input
+ */
+ minLength?: number;
+
+ /**
+ * For text input, this checks the maximum length of text for valid input
+ */
+ maxLength?: number;
+
+ /**
+ * For text input, this checks the text agains a Regular expression pattern.
+ */
+ pattern?: string;
+
+ /**
+ * A custom javascript based validation or a JSON object for using JSON Logic
+ */
+ custom?: any;
+
+ /**
+ * If the custom validation should remain private (only the backend will see it and execute it).
+ */
+ customPrivate?: boolean;
+
+ /**
+ * Minimum value for numbers
+ */
+ min?: number;
+
+ /**
+ * Maximum value for numbers
+ */
+ max?: number;
+
+ minSelectedCount?: number;
+ maxSelectedCount?: number;
+ minWords?: number;
+ maxWords?: number;
+ email?: boolean;
+ url?: boolean;
+ date?: boolean;
+ day?: boolean;
+ json?: string;
+ mask?: boolean;
+ minDate?: any;
+ maxDate?: any;
+}
+
+/**
+ * The Formio interface class. This is a minimalistic API library that allows you to work with the Form.io API's within JavaScript.
+ *
+ * ## Usage
+ * Creating an instance of Formio is simple, and takes only a path (URL String). The path can be different, depending on the desired output.
+ * The Formio instance can also access higher level operations, depending on how granular of a path you start with.
+ *
+ * ```ts
+ * var formio = new Formio(, [options]);
+ * ```
+ *
+ * Where **endpoint** is any valid API endpoint within Form.io. These URL's can provide a number of different methods depending on the granularity of the endpoint. This allows you to use the same interface but have access to different methods depending on how granular the endpoint url is.
+ * **options** is defined within the {link Formio.constructor} documentation.
+ *
+ * Here is an example of how this library can be used to load a form JSON from the Form.io API's
+ *
+ * ```ts
+ * const formio = new Formio('https://examples.form.io/example');
+ * formio.loadForm().then((form) => {
+ * console.log(form);
+ * });
+ * ```
+ */
+export declare class Formio {
+ path?: string | undefined;
+ options: FormioOptions;
+ /**
+ * The base API url of the Form.io Platform. Example: https://api.form.io
+ */
+ static baseUrl: string;
+ /**
+ * The project API url of the Form.io Project. Example: https://examples.form.io
+ */
+ static projectUrl: string;
+ /**
+ * The project url to use for Authentication.
+ */
+ static authUrl: string;
+ /**
+ * The path type for the project.
+ */
+ static pathType?: FormioPathType;
+ /**
+ * Set to true if the project url has been established with ```Formio.setProjectUrl()```
+ */
+ static projectUrlSet: boolean;
+ /**
+ * The Form.io API Cache. This ensures that requests to the same API endpoint are cached.
+ */
+ static cache: any;
+ /**
+ * The namespace used to save the Form.io Token's and variables within an application.
+ */
+ static namespace: string;
+ /**
+ * Handles events fired within this SDK library.
+ */
+ static events: EventEmitter;
+ /**
+ * Stores all of the libraries lazy loaded with ```Formio.requireLibrary``` method.
+ */
+ static libraries: any;
+ /**
+ * The Library license for this application.
+ */
+ static license: string;
+ /**
+ * A direct interface to the Form.io fetch polyfill.
+ */
+ static fetch: any;
+ /**
+ * A direct interface to the Form.io fetch Headers polyfill.
+ */
+ static Headers: any;
+ /**
+ * The rules definitions.
+ */
+ static rulesEntities: any;
+ /**
+ * All of the auth tokens for this session.
+ */
+ static tokens: any;
+ static config: any;
+ static icons: string;
+ /**
+ * The version of this library.
+ */
+ static version: string;
+ static formOnly: boolean;
+ /**
+ * The base API url of the Form.io Platform. Example: https://api.form.io
+ */
+ base: string;
+ /**
+ * The Projects Endpoint derived from the provided source.
+ *
+ * @example https://api.form.io/project
+ */
+ projectsUrl: string;
+ /**
+ * A specific project endpoint derived from the provided source.
+ *
+ * @example https://examples.form.io
+ */
+ projectUrl: string;
+ /**
+ * The Project ID found within the provided source.
+ */
+ projectId: string;
+ /**
+ * A specific Role URL provided the source.
+ *
+ * @example https://examples.form.io/role/2342343234234234
+ */
+ roleUrl: string;
+ /**
+ * The roles endpoint derived from the provided source.
+ *
+ * @example https://examples.form.io/role
+ */
+ rolesUrl: string;
+ /**
+ * The roleID derieved from the provided source.
+ */
+ roleId: string;
+ /**
+ * A specific form url derived from the provided source.
+ *
+ * @example https://examples.form.io/example
+ */
+ formUrl: string;
+ /**
+ * The forms url derived from the provided source.
+ *
+ * @example https://example.form.io/form
+ */
+ formsUrl: string;
+ /**
+ * The Form ID derived from the provided source.
+ */
+ formId: string;
+ /**
+ * The submissions URL derived from the provided source.
+ *
+ * @example https://examples.form.io/example/submission
+ */
+ submissionsUrl: string;
+ /**
+ * A specific submissions URL derived from a provided source.
+ *
+ * @example https://examples.form.io/example/submission/223423423423
+ */
+ submissionUrl: string;
+ /**
+ * The submission ID provided a submission url.
+ */
+ submissionId: string;
+ /**
+ * The actions url provided a form url as the source.
+ *
+ * @example https://examples.form.io/example/action
+ */
+ actionsUrl: string;
+ /**
+ * The Action ID derived from a provided Action url.
+ */
+ actionId: string;
+ /**
+ * A specific action api endoint.
+ */
+ actionUrl: string;
+ vsUrl: string;
+ vId: string;
+ vUrl: string;
+ /**
+ * The query string derived from the provided src url.
+ */
+ query: string;
+ /**
+ * The project type.
+ */
+ pathType?: FormioPathType;
+ /**
+ * If this is a non-project url, such is the case for Open Source API.
+ */
+ noProject: boolean;
+ /**
+ * @constructor
+ * @param {string} path - A project, form, and submission API Url.
+ * @param {FormioOptions} options - Available options to configure the Javascript API.
+ */
+ constructor(path?: string | undefined, options?: FormioOptions);
+ /**
+ * Deletes a remote resource of any provided type.
+ *
+ * @param {string} type - The type of resource to delete. "submission", "form", etc.
+ * @param {object} options - The options passed to {@link Formio.request}
+ * @return {Promise}
+ */
+ delete(type: string, opts?: any): any;
+ /**
+ * Returns the index (array of records) for any provided type.
+ *
+ * @param {string} type - The type of resource to fetch the index of. "submission", "form", etc.
+ * @param {object} query - A query object to pass to the request.
+ * @param {object} query.params - A map (key-value pairs) of URL query parameters to add to the url.
+ * @param {object} options - Options to pass to {@link Formio.request}
+ * @return {Promise}
+ */
+ index(type: string, query?: any, opts?: any): any;
+ /**
+ * Save a document record using "upsert". If the document does not exist, it will be created, if the _id is provided,
+ * it will be updated.
+ *
+ * @param {string} type - The type of resource to fetch the index of. "submission", "form", etc.
+ * @param {object} data - The resource data object.
+ * @param {object} options - Options to pass to {@link Formio.request}
+ * @return {Promise |