Skip to content

Commit

Permalink
feat: create form component
Browse files Browse the repository at this point in the history
  • Loading branch information
rafaelcamargo committed Nov 6, 2020
1 parent 3234106 commit 47af4ec
Show file tree
Hide file tree
Showing 38 changed files with 2,357 additions and 177 deletions.
6 changes: 3 additions & 3 deletions .eslintrc.json
Expand Up @@ -33,7 +33,7 @@
"quotes": ["error", "single"],
"semi": ["error", "always"],
"complexity": ["error", { "max": 3 }],
"max-lines": ["error", { "max": 100 }],
"max-lines": ["error", { "max": 110 }],
"max-statements": ["error", { "max": 5 },
{ "ignoreTopLevelFunctions": true }
]
Expand All @@ -42,8 +42,8 @@
{
"files": [ "src/**/*.test.js" ],
"rules": {
"max-lines": ["error", { "max": 250 }],
"max-statements": ["error", { "max": 10 },
"max-lines": ["error", { "max": 300 }],
"max-statements": ["error", { "max": 12 },
{ "ignoreTopLevelFunctions": true }
]
}
Expand Down
4 changes: 3 additions & 1 deletion src/base/constants/form.js
@@ -1,5 +1,7 @@
const FORM_ID_CUSTOM_ATTR = 'data-form-id';
const REQUEST_ERROR_MESSAGE = 'Something went wrong. Please, try again.';

export {
FORM_ID_CUSTOM_ATTR
FORM_ID_CUSTOM_ATTR,
REQUEST_ERROR_MESSAGE
};
3 changes: 2 additions & 1 deletion src/base/mocks/form-control-model.js
@@ -1,6 +1,7 @@
export const formControlModelInstanceMock = {
setElementValue: jest.fn(),
onRequiredChange: jest.fn()
onRequiredChange: jest.fn(),
destroy: jest.fn()
};

export const FormControlModelMock = jest.fn((el, options) => {
Expand Down
40 changes: 25 additions & 15 deletions src/base/models/form-control/form-control.js
Expand Up @@ -6,8 +6,10 @@ export class FormControlModel {
this.setId(idService.generate());
this.configElement(formControlEl, options);
this.setOptions(options);
this.configForm(formControlEl);
this.configValidations(formControlEl, options.validations);
setTimeout(() => {
this.configForm(formControlEl);
this.configValidations(formControlEl, options.validations);
});
}
setId(id){
this.id = id;
Expand All @@ -25,19 +27,18 @@ export class FormControlModel {
this.element.required = required;
}
handleAutofocus(){
const { element } = this;
if(element.getAttribute('autofocus')) element.focus();
if(this.element.getAttribute('autofocus')) this.element.focus();
}
setOptions(options){
this.options = options;
}
configForm(formControlEl){
this.setForm(formControlService.findParentFormModel(formControlEl));
if(this.form)
this.form.onSubmit(() => {
this.setSubmitListenerId(this.form.onSubmit(() => {
this.setBlurred(true);
this.validate(formControlEl);
});
}));
}
setForm(form){
this.form = form;
Expand Down Expand Up @@ -67,10 +68,9 @@ export class FormControlModel {
this.validate(this.element);
}
validate({ value }){
const errors = [];
this.buildValidations().forEach(({ isValid, errorMessage }) => {
if(!isValid(value)) errors.push(errorMessage);
});
const errors = this.buildValidations().map(({ isValid, errorMessage }) => {
if(!isValid(value)) return errorMessage;
}).filter(err => !!err);
return errors.length ? this.emitError(errors[0]) : this.emitSuccess();
}
buildValidations(){
Expand All @@ -79,18 +79,28 @@ export class FormControlModel {
return validations;
}
emitError(err){
if(this.form) this.form.setError(this.id, err);
if(this.hasBeenBlured) this.handleCallbackOption('onValidate', err);
if(this.form) this.form.setError(this.id, { element: this.element, message: err});
if(this.hasBeenBlurred) this.handleCallbackOption('onValidate', err);
}
emitSuccess(){
if(this.form) this.form.clearError(this.id);
if(this.hasBeenBlured) this.handleCallbackOption('onValidate');
if(this.hasBeenBlurred) this.handleCallbackOption('onValidate');
}
handleCallbackOption(option, data){
const callback = this.options[option];
return callback && callback(data);
}
setBlurred(hasBeenBlured){
this.hasBeenBlured = hasBeenBlured;
setBlurred(hasBeenBlurred){
this.hasBeenBlurred = hasBeenBlurred;
}
setSubmitListenerId(id){
this.submitListenerId = id;
}
destroy(){
this.form && this.clearControlFromParentFormModel();
}
clearControlFromParentFormModel(){
this.form.clearError(this.id);
this.form.removeSubmitListener(this.submitListenerId);
}
}
59 changes: 43 additions & 16 deletions src/base/models/form-control/form-control.test.js
Expand Up @@ -3,11 +3,14 @@ import idService from '@base/services/id/id';
import formService from '@base/services/form/form';
import { FormControlModel } from './form-control';

jest.useFakeTimers();

describe('Form Control Model', () => {
function mockFormControlElement({ autofocus } = {}){
function mockFormControlElement({ autofocus, required } = {}){
const input = document.createElement('input');
input.setAttribute('type', 'text');
if(autofocus) input.setAttribute('autofocus', 'autofocus');
if(required) input.setAttribute('required', 'required');
return input;
}

Expand All @@ -16,11 +19,18 @@ describe('Form Control Model', () => {
const form = formService.build(formEl);
form.setError = jest.fn();
form.clearError = jest.fn();
form.removeSubmitListener = jest.fn();
if(formControlEl)
formEl.appendChild(formControlEl);
return form;
}

function instantiateFormControl(formControlEl, options){
const formControl = new FormControlModel(formControlEl, options);
jest.runOnlyPendingTimers();
return formControl;
}

it('should identify form control', () => {
idService.generate = jest.fn(() => '123');
const formControlEl = mockFormControlElement();
Expand All @@ -39,14 +49,19 @@ describe('Form Control Model', () => {
const formControlEl = mockFormControlElement();
const form = mockForm(formControlEl);
form.setError = jest.fn();
const formControl = new FormControlModel(formControlEl, { required: true });
expect(form.setError).toHaveBeenCalledWith(formControl.id, REQUIRED_ERROR_MESSAGE);
const formControl = instantiateFormControl(formControlEl, { required: true });
expect(form.setError).toHaveBeenCalledWith(
formControl.id, {
element: formControlEl,
message: REQUIRED_ERROR_MESSAGE
}
);
});

it('should config form control element listeners', () => {
const formControlEl = mockFormControlElement();
formControlEl.addEventListener = jest.fn((type, callback) => callback({ target: {} }));
new FormControlModel(formControlEl);
instantiateFormControl(formControlEl);
expect(formControlEl.addEventListener.mock.calls[0][0]).toEqual('input');
expect(typeof formControlEl.addEventListener.mock.calls[0][1]).toEqual('function');
expect(formControlEl.addEventListener.mock.calls[1][0]).toEqual('blur');
Expand All @@ -56,14 +71,14 @@ describe('Form Control Model', () => {
it('should focus form control element if autofocus attribute is present', () => {
const formControlEl = mockFormControlElement({ autofocus: true });
formControlEl.focus = jest.fn();
new FormControlModel(formControlEl);
instantiateFormControl(formControlEl);
expect(formControlEl.focus).toHaveBeenCalled();
});

it('should execute validation callback if required form control is blank on blur', () => {
const formControlEl = mockFormControlElement();
const onValidate = jest.fn();
const formControl = new FormControlModel(formControlEl, { onValidate, required: true });
const formControl = instantiateFormControl(formControlEl, { onValidate, required: true });
const evtMock = { target: { value: '' } };
formControl.onBlur(evtMock);
expect(onValidate).toHaveBeenCalledWith(REQUIRED_ERROR_MESSAGE);
Expand All @@ -72,7 +87,7 @@ describe('Form Control Model', () => {
it('should execute validation callback when required changes and form control has already been blurred', () => {
const formControlEl = mockFormControlElement();
const onValidate = jest.fn();
const formControl = new FormControlModel(formControlEl, { onValidate, required: true });
const formControl = instantiateFormControl(formControlEl, { onValidate, required: true });
formControl.onRequiredChange(false);
const evtMock = { target: { value: '' } };
formControl.onBlur(evtMock);
Expand All @@ -82,7 +97,7 @@ describe('Form Control Model', () => {
it('should not execute validation callback when required changes but form control has not been blurred yet', () => {
const formControlEl = mockFormControlElement();
const onValidate = jest.fn();
const formControl = new FormControlModel(formControlEl, { onValidate, required: true });
const formControl = instantiateFormControl(formControlEl, { onValidate, required: true });
formControl.onRequiredChange(false);
expect(onValidate).not.toHaveBeenCalled();
});
Expand All @@ -92,7 +107,7 @@ describe('Form Control Model', () => {
const errorMessage = 'Enter a valid name';
const validations = [{ isValid: data => data == 'Rafael', errorMessage }];
const onValidate = jest.fn();
const formControl = new FormControlModel(formControlEl, { validations, onValidate });
const formControl = instantiateFormControl(formControlEl, { validations, onValidate });
const evtMock = { target: { value: 'John' } };
formControl.onBlur(evtMock);
expect(onValidate).toHaveBeenCalledWith(errorMessage);
Expand All @@ -101,7 +116,7 @@ describe('Form Control Model', () => {
it('should execute validation callback if custom validation succeed on blur', () => {
const formControlEl = mockFormControlElement();
const onValidate = jest.fn();
const formControl = new FormControlModel(formControlEl, { onValidate, required: true });
const formControl = instantiateFormControl(formControlEl, { onValidate, required: true });
const evtMock = { target: { value: 'John' } };
formControl.onBlur(evtMock);
expect(onValidate).toHaveBeenCalledWith(undefined);
Expand All @@ -110,7 +125,7 @@ describe('Form Control Model', () => {
it('should not execute validation callback if form control has not been blurred yet', () => {
const formControlEl = mockFormControlElement({ required: true });
const onValidate = jest.fn();
const formControl = new FormControlModel(formControlEl, { onValidate });
const formControl = instantiateFormControl(formControlEl, { onValidate });
const evtMock = { target: { value: '' } };
formControl.onInput(evtMock);
expect(onValidate).not.toHaveBeenCalled();
Expand All @@ -120,15 +135,15 @@ describe('Form Control Model', () => {
const formControlEl = mockFormControlElement();
const form = mockForm(formControlEl);
const onValidate = jest.fn();
new FormControlModel(formControlEl, { onValidate, required: true });
instantiateFormControl(formControlEl, { onValidate, required: true });
form.handleSubmit({ preventDefault: jest.fn() });
expect(onValidate).toHaveBeenCalledWith(REQUIRED_ERROR_MESSAGE);
});

it('should execute change callback on change', () => {
const formControlEl = mockFormControlElement({ required: true });
const onInput = jest.fn();
const formControl = new FormControlModel(formControlEl, { onInput });
const formControl = instantiateFormControl(formControlEl, { onInput });
const evtMock = { target: { value: 'R' } };
formControl.onInput(evtMock);
expect(onInput).toHaveBeenCalledWith(evtMock);
Expand All @@ -137,18 +152,30 @@ describe('Form Control Model', () => {
it('should set validation error to form if form control is invalid on change', () => {
const formControlEl = mockFormControlElement();
const form = mockForm(formControlEl);
const formControl = new FormControlModel(formControlEl, { required: true });
const formControl = instantiateFormControl(formControlEl, { required: true });
const evtMock = { target: { value: '' } };
formControl.onInput(evtMock);
expect(form.setError).toHaveBeenCalledWith(formControl.id, REQUIRED_ERROR_MESSAGE);
expect(form.setError).toHaveBeenCalledWith(formControl.id, {
element: formControlEl,
message: REQUIRED_ERROR_MESSAGE
});
});

it('should clear validation error from form if form control is valid on change', () => {
const formControlEl = mockFormControlElement({ required: true });
const form = mockForm(formControlEl);
const formControl = new FormControlModel(formControlEl);
const formControl = instantiateFormControl(formControlEl);
const evtMock = { target: { value: 'Rafael' } };
formControl.onInput(evtMock);
expect(form.clearError).toHaveBeenCalledWith(formControl.id);
});

it('should clear its errors and remove its submit listeners from parent form model on destroy', () => {
const formControlEl = mockFormControlElement({ required: true });
const form = mockForm(formControlEl);
const formControl = instantiateFormControl(formControlEl);
formControl.destroy();
expect(form.clearError).toHaveBeenCalledWith(formControl.id);
expect(form.removeSubmitListener).toHaveBeenCalledWith(formControl.submitListenerId);
});
});

0 comments on commit 47af4ec

Please sign in to comment.