Skip to content

Commit

Permalink
[New #153] Add basic validation logic
Browse files Browse the repository at this point in the history
  • Loading branch information
VojtechLunak authored and LaChope committed Mar 4, 2024
1 parent 48c6cc4 commit 5b6d07a
Show file tree
Hide file tree
Showing 10 changed files with 231 additions and 65 deletions.
1 change: 0 additions & 1 deletion src/components/Answer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,6 @@ const Answer = (props) => {
answer={props.answer}
/>
);
return null;
};

const _renderRegularInput = (value, label, title) => {
Expand Down
41 changes: 37 additions & 4 deletions src/components/DefaultInput.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,34 @@ export default class DefaultInput extends React.Component {
);
}

_renderHelp() {
return this.props.help ? <FormText>{this.props.help}</FormText> : null;
_renderHelp(classname = "") {
return this.props.help ? (
<FormText className={classname}>{this.props.help}</FormText>
) : null;
}

_hasValidationWarning() {
return this.props.validation && this.props.validation === "warning";
}

_hasValidationError() {
return this.props.validation && this.props.validation === "error";
}

_hasValidationSuccess() {
return this.props.validation && this.props.validation === "success";
}

_getValidationClassname() {
if (this.props.validation && this.props.validation === "error") {
return "is-invalid";
}

if (this.props.validation && this.props.validation === "warning") {
return "is-warning";
}

return "";
}

_renderInput() {
Expand All @@ -142,13 +168,20 @@ export default class DefaultInput extends React.Component {
<FormGroup size="small">
{this._renderLabel()}
<FormControl
className={this._getValidationClassname()}
ref={(c) => (this.input = c)}
as="input"
{...this.props}
onChange={(e) => this.saveCursorPosition(e)}
/>
{this.props.validation && <FormControl.Feedback />}
{this._renderHelp()}
{(this._hasValidationSuccess() || this._hasValidationWarning()) &&
this._renderHelp(this._getValidationClassname())}
{this._hasValidationError() && (
<FormControl.Feedback type={"invalid"}>
{this.props.help}
</FormControl.Feedback>
)}
{!this.props.validation && this._renderHelp()}
</FormGroup>
);
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/Question.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export default class Question extends React.Component {
super(props);
JsonLdObjectMap.putObject(props.question["@id"], props.question);
this.state = {
validator: null,
validator: {},
expanded: !FormUtils.isCollapsed(props.question),
showIcon: false,
};
Expand Down
10 changes: 9 additions & 1 deletion src/components/answer/InputAnswer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,16 @@ class InputPropertiesResolver {
}
props.disabled =
componentsOptions.readOnly || FormUtils.isDisabled(question);

if (question[Constants.HAS_VALID_ANSWER] === false) {
props.validation = "error";
if (
question[Constants.HAS_VALIDATION_SEVERITY] ===
Constants.VALIDATION_SEVERITY.WARNING
) {
props.validation = "warning";
} else {
props.validation = "error";
}
props.help = question[Constants.HAS_VALIDATION_MESSAGE];
}

Expand Down
12 changes: 12 additions & 0 deletions src/constants/Constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export default class Constants {
BOOLEAN: "http://www.w3.org/2001/XMLSchema#boolean",
};
static STEP = "http://onto.fel.cvut.cz/ontologies/form/step";
static PATTERN = "http://onto.fel.cvut.cz/ontologies/form/pattern";
static ACCEPTS_ANSWER_VALUE =
"http://onto.fel.cvut.cz/ontologies/form/accepts-answer-value";
static ACCEPTS = "http://onto.fel.cvut.cz/ontologies/form/accepts";
Expand Down Expand Up @@ -111,6 +112,10 @@ export default class Constants {
"http://onto.fel.cvut.cz/ontologies/form/negative-condition";
static REQUIRES_ANSWER =
"http://onto.fel.cvut.cz/ontologies/form/requires-answer";
static USED_ONLY_FOR_COMPLETENESS =
"http://onto.fel.cvut.cz/ontologies/form/used-only-for-completeness";
static REQUIRES_ANSWER_DESCRIPTION_IF =
"http://onto.fel.cvut.cz/ontologies/form/requires-answer-description-if";
static REQUIRES_ANSWER_IF =
"http://onto.fel.cvut.cz/ontologies/form/requires-answer-if";
static HAS_PRECEDING_QUESTION =
Expand Down Expand Up @@ -143,6 +148,8 @@ export default class Constants {
"http://onto.fel.cvut.cz/ontologies/form/not-answered-question";
static ANSWERED_QUESTION =
"http://onto.fel.cvut.cz/ontologies/form/answered-question";
static HAS_VALIDATION_SEVERITY =
"http://onto.fel.cvut.cz/ontologies/form/has-validation-severity";

static RDFS_LABEL = JsonLdUtils.RDFS_LABEL;
static RDFS_COMMENT = JsonLdUtils.RDFS_COMMENT;
Expand Down Expand Up @@ -193,4 +200,9 @@ export default class Constants {
label: "English",
},
};

static VALIDATION_SEVERITY = {
ERROR: "error",
WARNING: "warning",
};
}
189 changes: 134 additions & 55 deletions src/model/ValidatorFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,69 +9,148 @@ import FormUtils from "../util/FormUtils";

export default class ValidatorFactory {
static createValidator(question, intl) {
if (question[Constants.REQUIRES_ANSWER]) {
if (FormUtils.isCheckbox(question)) {
//TODO revise
return ValidatorFactory._generateRequiresAnswerCheckBoxValidator(
question,
intl
);
const validators = [
this._patternValidator,
this._checkboxValidator,
this._requiredValidator,
this._completenessValidator,
];

return (answer) => {
if (FormUtils.hasValidationLogic(question)) {
const answerValue = this._getAnswerValue(answer);
return this._validateAnswer(question, intl, answerValue, validators);
}
return ValidatorFactory._generateRequiresAnswerValidator(question, intl);
} else {
return () => {
const result = {};
result[Constants.HAS_VALID_ANSWER] = true;
delete result[Constants.HAS_VALIDATION_MESSAGE];
};
}

static _isQuestionAnswered(answerValue) {
return (
answerValue !== null && answerValue !== undefined && answerValue !== ""
);
}

static _isCheckboxAnswered(answerValue) {
return (
answerValue !== null &&
answerValue !== undefined &&
answerValue !== "" &&
answerValue !== false
);
}

static _validateAnswer(question, intl, answerValue, validators) {
const result = {};
for (const validator of validators) {
const validationResult = validator(question, intl, answerValue);
if (!validationResult.isValid) {
result[Constants.HAS_VALID_ANSWER] = false;
result[Constants.HAS_VALIDATION_MESSAGE] = validationResult.message;
result[Constants.HAS_VALIDATION_SEVERITY] =
validationResult.validationSeverity;
return result;
};
}
if (result[Constants.HAS_VALID_ANSWER] === false) {
break;
}
}
result[Constants.HAS_VALID_ANSWER] = true;
return result;
}

static _generateRequiresAnswerValidator(question, intl) {
return (answer) => {
let val = null;
if (answer[Constants.HAS_DATA_VALUE]) {
val = JsonLdUtils.getJsonAttValue(answer, Constants.HAS_DATA_VALUE);
} else if (answer[Constants.HAS_OBJECT_VALUE]) {
val = JsonLdUtils.getJsonAttValue(
answer,
Constants.HAS_OBJECT_VALUE,
"@id"
);
static _patternValidator(question, intl, answerValue) {
if (answerValue && answerValue.length > 0) {
if (question[Constants.PATTERN]) {
let pattern = question[Constants.PATTERN];
const regExp = new RegExp(pattern);
const isValid =
regExp.test(answerValue) ||
!ValidatorFactory._isQuestionAnswered(answerValue);
if (!isValid) {
return {
isValid: false,
validationSeverity: Constants.VALIDATION_SEVERITY.ERROR,
message: question[Constants.HAS_VALIDATION_MESSAGE]
? question[Constants.HAS_VALIDATION_MESSAGE]
: "Please enter a valid answer to " +
JsonLdUtils.getLocalized(
question[JsonLdUtils.RDFS_LABEL],
intl
),
};
}
}
const isValid = val !== null && val !== undefined && val !== "";
const result = {};
result[Constants.HAS_VALID_ANSWER] = isValid;
result[Constants.HAS_VALIDATION_MESSAGE] = isValid
? null
: JsonLdUtils.getLocalized(question[JsonLdUtils.RDFS_LABEL], intl) +
" is missing a value.";
return result;
};
}
return { isValid: true };
}

static _generateRequiresAnswerCheckBoxValidator(question, intl) {
return (answer) => {
let val = null;
if (answer[Constants.HAS_DATA_VALUE]) {
val = JsonLdUtils.getJsonAttValue(answer, Constants.HAS_DATA_VALUE);
} else if (answer[Constants.HAS_OBJECT_VALUE]) {
val = JsonLdUtils.getJsonAttValue(
answer,
Constants.HAS_OBJECT_VALUE,
"@id"
);
static _requiredValidator(question, intl, answerValue) {
if (
question[Constants.REQUIRES_ANSWER] &&
!question[Constants.USED_ONLY_FOR_COMPLETENESS]
) {
const isValid = ValidatorFactory._isQuestionAnswered(answerValue);
if (!isValid) {
return {
isValid: false,
validationSeverity: Constants.VALIDATION_SEVERITY.ERROR,
message:
JsonLdUtils.getLocalized(question[JsonLdUtils.RDFS_LABEL], intl) +
" is required",
};
}
const isValid =
val !== null && val !== undefined && val !== "" && val !== false;
const result = {};
result[Constants.HAS_VALID_ANSWER] = isValid;
result[Constants.HAS_VALIDATION_MESSAGE] = isValid
? null
: JsonLdUtils.getLocalized(question[JsonLdUtils.RDFS_LABEL], intl) +
" must be checked.";
return result;
};
}
return { isValid: true };
}

static _checkboxValidator(question, intl, answerValue) {
if (FormUtils.isCheckbox(question)) {
if (question[Constants.REQUIRES_ANSWER]) {
const isValid = ValidatorFactory._isCheckboxAnswered(answerValue);
if (!isValid) {
return {
isValid: false,
validationSeverity: Constants.VALIDATION_SEVERITY.ERROR,
message:
JsonLdUtils.getLocalized(question[JsonLdUtils.RDFS_LABEL], intl) +
" must be checked",
};
}
}
}
return { isValid: true };
}

static _completenessValidator(question, intl, answerValue) {
if (
question[Constants.REQUIRES_ANSWER] &&
question[Constants.USED_ONLY_FOR_COMPLETENESS]
) {
const isValid = ValidatorFactory._isQuestionAnswered(answerValue);
if (!isValid) {
return {
isValid: false,
validationSeverity: Constants.VALIDATION_SEVERITY.WARNING,
message:
JsonLdUtils.getLocalized(question[JsonLdUtils.RDFS_LABEL], intl) +
" should be filled to complete the form.",
};
}
}
return { isValid: true };
}

static _getAnswerValue(answer) {
let val = null;
if (answer[Constants.HAS_DATA_VALUE]) {
val = JsonLdUtils.getJsonAttValue(answer, Constants.HAS_DATA_VALUE);
} else if (answer[Constants.HAS_OBJECT_VALUE]) {
val = JsonLdUtils.getJsonAttValue(
answer,
Constants.HAS_OBJECT_VALUE,
"@id"
);
}
return val;
}
}
2 changes: 1 addition & 1 deletion src/stories/assets/form/form1.json
Original file line number Diff line number Diff line change
Expand Up @@ -556,7 +556,7 @@
"has-layout-class": ["section", "checkbox", "answerable"],
"@id": "answerable-section-4808",
"has_related_question": ["aircraft-name-9553", "aircraft-number-2375"],
"@type": "doc:question"
"@type": "doc:question",
},
{
"label": "Category 1",
Expand Down
21 changes: 19 additions & 2 deletions src/stories/assets/form/form2.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,15 @@
},
"provides-dereferenceable-answer-values": {
"@id": "http://onto.fel.cvut.cz/ontologies/form/provides-dereferenceable-answer-values"
},
"used-only-for-completeness": {
"@id": "http://onto.fel.cvut.cz/ontologies/form/used-only-for-completeness"
},
"has-validation-message": {
"@id": "http://onto.fel.cvut.cz/ontologies/form/has-validation-message"
},
"pattern": {
"@id": "http://onto.fel.cvut.cz/ontologies/form/pattern"
}
},
"@graph": [
Expand Down Expand Up @@ -159,15 +168,23 @@
"@type": "doc:question",
"has_related_question": [],
"has-layout-class": "text",
"label": "First name"
"label": "First name (test completeness and invalid values)",
"requires-answer": true,
"used-only-for-completeness": true,
"pattern": "^[A-Za-z]+$",
"description": "This question requires answer only to complete the form (i.e. it is not necessary for saving the form). When you loose focus of the field, the validation is triggered. It has custom validation message which will be triggered if you write name that is not compliant with pattern \"^[A-Za-z]+$\""
},
{
"@id": "last-name-1495",
"@type": "doc:question",
"has_related_question": [],
"has-layout-class": "text",
"has-preceding-question": "first-name-6663",
"label": "Last name"
"label": "Last name (testing required and invalid values)",
"requires-answer": true,
"pattern": "^[A-Za-z]+$",
"has-validation-message": "Answer can only use characters.",
"description": "This question requires answer to save the form. When you loose focus of the field, the validation is triggered. It has custom validation message which will be triggered if you write name that is not compliant with pattern \"^[A-Za-z]+$\""
},
{
"@id": "job-9002",
Expand Down
Loading

0 comments on commit 5b6d07a

Please sign in to comment.