Skip to content

Instantly share code, notes, and snippets.

@loranallensmith
Created March 26, 2020 16:01
Show Gist options
  • Save loranallensmith/3f4d29c8d025ba24590b7f604bd77fd9 to your computer and use it in GitHub Desktop.
Save loranallensmith/3f4d29c8d025ba24590b7f604bd77fd9 to your computer and use it in GitHub Desktop.
JavaScript Example
/**
* Household Builder
*/
/**
* Model Layer
*/
/**
* Creates a new household member.
*
* @param {number} age the age of the household member in years
* @param {string} relationship the relationship between the household member and the applicant
* @param {boolean} isSmoker flag indicating whether or not the household member is a smoker
* @class
*/
function HouseholdMember(age, relationship, isSmoker) {
this.age = {
value: age,
validation: {
rules: ['isNotEmpty', 'isInt', 'isGreaterThanZero'],
hint: 'Age needs to be a whole number greater than zero.'
}
};
this.relationship = {
value: relationship,
validation: {
rules: ['isNotEmpty'],
hint: 'Please let us know this person\'s relationship to you.'
}
};
this.isSmoker = {
value: isSmoker
};
this.validator = new HouseholdMemberValidator();
}
/**
* Sets a HouseholdMember property value at a specified key if it passes validation.
*
* @memberof HouseholdMember
* @instance
* @param {*} value the value to assign
* @param {*} key the property to modify
* @return {boolean} true if the value was valid and set and false otherwise
*/
HouseholdMember.prototype.set = function set(value, key) {
if (this.validator.validate(value, key.validation.rules)) {
key.value = value;
return true;
}
return false;
};
/**
* Contains business rules for determining whether or not a HouseholdMember is valid.
*
* @memberof HouseholdMember
* @instance
* @return {boolean} true if the HouseholdMember's validation rules are satisfied.
*/
HouseholdMember.prototype.isValid = function isValid() {
// Clear out existing validation errors before testing validity
this.validationErrors = [];
// Check any properties with validation rules and push any errors to the errors array.
for (var field in this) {
if (this.hasOwnProperty(field) && this[field].hasOwnProperty('validation')) {
if (!this.validator.validate(this[field].value, this[field].validation.rules)) {
this.validationErrors.push(this[field].validation.hint);
}
}
}
// If there are no validation errors, return true.
if (this.validationErrors.length === 0) {
return true;
}
return false;
};
/**
* Overrides the default toString() method to print household member data as a simple string.
*
* @memberof HouseholdMember
* @instance
* @return {string} relationship, age, isSmoker
*/
HouseholdMember.prototype.toString = function toString() {
return [
this.relationship.value,
this.age.value,
this.isSmoker.value ? 'Smoker' : 'Nonsmoker'
].join(', ');
};
/**
* Creates a new validator for a HouseholdMember to use for validating its properties.
*
* @class
*/
function HouseholdMemberValidator() {
}
/**
* Determines whether a specified value satisfies the supplied array of rules.
*
* @memberof HouseholdMemberValidator
* @instance
* @param {*} value the value to validate
* @param {string[]} rules an array of rules that must be satisfied, defined as functions on the HouseholdMemberValidator object
* @return {boolean} true if the value satisfies all of the rules and false otherwise
*/
HouseholdMemberValidator.prototype.validate = function validate(value, rules) {
var self = this;
return rules.every(function validateRule(rule) {
return self[rule](value);
});
};
/**
* Validates that a value is not empty.
*
* @memberof HouseholdMemberValidator
* @instance
* @param {*} value the value to validate
* @return {boolean} true if the value is not empty, null, or undefined
*/
HouseholdMemberValidator.prototype.isNotEmpty = function isNotEmpty(value) {
if (value !== '' && value !== null && typeof value !== 'undefined') {
return true;
}
return false;
};
/**
* Validates that a value is an integer.
*
* @memberof HouseholdMemberValidator
* @instance
* @param {*} value the value to validate
* @return {boolean} true if the value is an integer and false otherwise
*/
HouseholdMemberValidator.prototype.isInt = function isInt(value) {
return Number.isInteger(Number(value));
};
/**
* Validates that a value is greater than zero.
*
* @memberof HouseholdMemberValidator
* @instance
* @param {*} value the value to validate
* @return {boolean} true if the value is greater than zero and false otherwise
*/
HouseholdMemberValidator.prototype.isGreaterThanZero = function isGreaterThanZero(value) {
return value > 0;
};
/**
* Validates that a value is a boolean.
*
* @memberof HouseholdMemberValidator
* @instance
* @param {*} value the value to validate
* @return {boolean} true if the value is boolean and false otherwise
*/
HouseholdMemberValidator.prototype.isBoolean = function isBoolean(value) {
return typeof value === 'boolean';
};
/**
* Creates a new Household object.
*
* @class
*/
function Household() {
this.householdMembers = [];
}
/**
* Add a valid household member to the household, assign it an auto-incrementing ID, and call its bound callback function.
*
* @memberof Household
* @instance
* @param {HouseholdMember} householdMember a HouseholdMember object
* @return {boolean} true if the HouseHold member was added to the Household and false otherwise
*/
Household.prototype.addHouseholdMember = function addHouseholdMember(householdMember) {
if (householdMember.isValid()) {
var newEntry = {
id: this.householdMembers.length > 0 ? this.householdMembers[this.householdMembers.length - 1].id + 1 : 1,
householdMember: householdMember
};
this.householdMembers.push(newEntry);
this.onHouseholdChanged(this.householdMembers);
return true;
}
return false;
};
/**
* Remove the HouseholdMember with the specified ID from the household.
*
* @memberof Household
* @instance
* @param {number} id the ID of the HouseholdMember to remove from the Household
* @return {void}
*/
Household.prototype.removeHouseholdMember = function removeHouseholdMember(id) {
this.householdMembers = this.householdMembers.filter(function testMember(member) {
return member.id !== id;
});
this.onHouseholdChanged(this.householdMembers);
};
/**
* Serialize the user-entered data about members in a household.
*
* @memberof Household
* @instance
* @return {string} A string containing the id, age, relationship, and smoker status of all members of a household.
*/
Household.prototype.serializeHouseholdMemberData = function serializeHouseholdMemberData() {
var householdData = [];
this.householdMembers.forEach(function buildAndAddSerializedMember(householdMember) {
var member = {
id: householdMember.id,
age: householdMember.householdMember.age.value,
relationship: householdMember.householdMember.relationship.value,
isSmoker: householdMember.householdMember.isSmoker.value
};
householdData.push(member);
});
return JSON.stringify(householdData);
};
/**
* Bind a callback function to be called when the Household changes.
*
* @memberof Household
* @instanc
* @param {function} callback the function to be called
* @return {void}
*/
Household.prototype.bindHouseholdChanged = function bindHouseholdChanged(callback) {
this.onHouseholdChanged = callback;
};
/**
* View Layer
*/
/**
* Create a new HouseholdBuilderView.
*
* @class
*/
function HouseholdBuilderView() {
// Assign UI elements in view
this.outlet = this.getElement('div.builder');
this.householdMembersListElement = this.getElement('ol.household');
this.householdMemberForm = this.getElement('form');
this.ageField = this.getElement('input[name="age"]');
this.relationshipField = this.getElement('select[name="rel"]');
this.smokerField = this.getElement('input[name="smoker"]');
this.addButton = this.getElement('button.add');
this.submitButton = this.getElement('button[type="submit"]');
this.debugElement = this.getElement('pre.debug');
}
/**
* Helper method for creating new DOM elements.
*
* @memberof HouseholdBuilderView
* @instance
* @param {string} tag the type of element to create
* @return {Element} the created element
*/
HouseholdBuilderView.prototype.createElement = function createElement(tag) {
var element = document.createElement(tag);
return element;
};
/**
* Helper method for querying DOM elements.
*
* @memberof HouseholdBuilderView
* @instance
* @param {string} selector the querySelector string
* @return {Element} the queried element
*/
HouseholdBuilderView.prototype.getElement = function getElement(selector) {
var element = document.querySelector(selector);
return element;
};
/**
* Helper method for querying sets of DOM elements.
*
* @memberof HouseholdBuilderView
* @instance
* @param {string} selector the querySelector string
* @return {Element[]} the queried elements
*/
HouseholdBuilderView.prototype.getElements = function getElements(selector) {
var elements = document.querySelectorAll(selector);
return elements;
};
/**
* Helper method for getting the HouseholdMember from the view's form fields.
*
* @memberof HouseholdBuilderView
* @instance
* @return {HouseholdMember} description
*/
HouseholdBuilderView.prototype.generateHouseholdMemberFromFormData = function generateHouseholdMemberFromFormData() {
return new HouseholdMember(this.ageField.value, this.relationshipField.value, this.smokerField.checked);
};
/**
* Helper method for resetting the form.
*
* @memberof HouseholdBuilderView
* @instance
* @return {void}
*/
HouseholdBuilderView.prototype.resetForm = function resetForm() {
this.ageField.value = '';
this.relationshipField.selectedIndex = 0;
this.smokerField.checked = false;
this.clearValidationHints();
};
/**
* Helper method for resetting the debug element.
*
* @memberof HouseholdBuilderView
* @instance
* @return {void}
*/
HouseholdBuilderView.prototype.resetDebugElement = function resetDebugElement() {
this.debugElement.textContent = '';
this.debugElement.style.display = 'none';
};
/**
* Display (or redisplay) the household members list in the DOM.
*
* @memberof HouseholdBuilderView
* @instance
* @param {Household} household the household to be displayed
* @return {void}
*/
HouseholdBuilderView.prototype.displayHouseholdMembers = function displayHouseholdMembers(household) {
// Clear out the current list of household members
while (this.householdMembersListElement.firstChild) {
this.householdMembersListElement.removeChild(this.householdMembersListElement.firstChild);
}
// Re-create and append LI elements for each household member
for (var i = 0; i < household.length; i++) {
var householdMemberListItem = this.createElement('li');
householdMemberListItem.setAttribute('data-id', household[i].id);
householdMemberListItem.textContent = household[i].householdMember.toString();
this.householdMembersListElement.append(householdMemberListItem);
var deleteButton = this.createElement('button');
deleteButton.className = 'delete';
deleteButton.textContent = 'Delete';
householdMemberListItem.append(deleteButton);
}
};
/**
* Display a list of validation hints in the UI, creating one initially if needed
*
* @param {HouseholdMember} householdMember the household member object with invalid data
* @return {void}
*/
HouseholdBuilderView.prototype.displayValidationHints = function displayValidationHints(householdMember) {
if (!this.getElement('div.validation-hints')) {
var validationHintsDiv = this.createElement('div');
validationHintsDiv.className = 'validation-hints';
this.outlet.insertBefore(validationHintsDiv, this.householdMemberForm);
} else {
validationHintsDiv = this.getElement('div.validation-hints');
validationHintsDiv.innerHTML = '';
}
var validationHintsTitle = this.createElement('h5');
validationHintsTitle.textContent = 'We\'re having some trouble adding that member. ' +
'Here are a few tips:';
validationHintsDiv.append(validationHintsTitle);
var validationHintsList = this.createElement('ul');
validationHintsList.className = 'validation-hints-list';
validationHintsDiv.append(validationHintsList);
householdMember.validationErrors.forEach(function getValidationErrors(error) {
var hintListItem = this.createElement('li');
hintListItem.textContent = error;
validationHintsList.append(hintListItem);
}.bind(this));
};
/**
* Remove the validation hints element from the UI
*
* @return {void}
*/
HouseholdBuilderView.prototype.clearValidationHints = function clearValidationHints() {
var validationHintsDiv = this.getElement('div.validation-hints');
if (validationHintsDiv) {
validationHintsDiv.parentNode.removeChild(validationHintsDiv);
}
};
/**
* Bind a callback function to be called when the view's add button is clicked.
*
* @memberof HouseholdBuilderView
* @instance
* @param {function} handler the function to be called
* @return {void}
*/
HouseholdBuilderView.prototype.bindAddHouseholdMember = function bindAddHouseholdMember(handler) {
var self = this;
this.addButton.addEventListener('click', function handleAddButtonClick(event) {
event.preventDefault();
var householdMember = self.generateHouseholdMemberFromFormData();
handler(householdMember);
});
};
/**
* Bind a callback function to be called when a household member's delete button is clicked.
*
* @memberof HouseholdBuilderView
* @instance
* @param {function} handler the function to be called
* @return {void}
*/
HouseholdBuilderView.prototype.bindRemoveHouseholdMember = function bindRemoveHouseholdMember(handler) {
this.householdMembersListElement.addEventListener('click', function handleDeleteButtonClick(event) {
if (event.target.className === 'delete') {
var id = Number(event.target.parentElement.getAttribute('data-id'));
handler(id);
}
});
};
/**
* Bind a callback function to be called when the view's submit button is clicked.
*
* @memberof HouseholdBuilderView
* @instance
* @param {function} handler the function to be called
* @return {void}
*/
HouseholdBuilderView.prototype.bindSubmitHousehold = function bindSubmitHousehold(handler) {
this.submitButton.addEventListener('click', function handleSubmitButtonClick(event) {
event.preventDefault();
handler();
});
};
/**
* Update the view's debug area with the household's serialized data
*
* @memberof HouseholdBuilderView
* @instance
* @param {string} serializedHousehold the serialized household data
* @return {void}
*/
HouseholdBuilderView.prototype.displaySerializedHousehold = function displaySerializedHousehold(serializedHousehold) {
if (serializedHousehold === '[]') {
this.debugElement.textContent = 'Please add at least one household member before submitting.';
} else {
this.debugElement.textContent = serializedHousehold;
}
this.debugElement.style.display = 'block';
};
/**
* Controller Layer
*/
/**
* Create a new HouseholdBuilderController object.
*
* @class
* @param {HouseholdBuilderView} householdBuilderView the HouseholdBuilderView instance managing UI interation.
* @param {Household} household the Household instance containing model data.
*/
function HouseholdBuilderController(householdBuilderView, household) {
this.householdBuilderView = householdBuilderView;
this.household = household;
}
/**
* Start the household builder.
*
* @memberof HouseholdBuilderController
* @instance
* @return {void}
*/
HouseholdBuilderController.prototype.start = function start() {
// Update listeners on instantiation
this.onHouseholdMembersChanged(this.household.householdMembers);
// Bind event handlers
this.householdBuilderView.bindAddHouseholdMember(this.handleAddHouseholdMember.bind(this));
this.householdBuilderView.bindRemoveHouseholdMember(this.handleRemoveHouseholdMember.bind(this));
this.householdBuilderView.bindSubmitHousehold(this.handleSubmitHousehold.bind(this));
this.household.bindHouseholdChanged(this.onHouseholdMembersChanged.bind(this));
};
/**
* Callback function for performing UI updates when the household's members have changed.
*
* @memberof HouseholdBuilderController
* @instance
* @param {Household} household the household to use for updating the UI
* @return {void}
*/
HouseholdBuilderController.prototype.onHouseholdMembersChanged = function onHouseholdMembersChanged(household) {
this.householdBuilderView.displayHouseholdMembers(household);
this.householdBuilderView.resetDebugElement();
};
/**
* Callback function for performing UI updates when an invalid household member is added.
*
* @memberof HouseholdBuilderController
* @instance
* @param {HouseholdMember} householdMember the invalid household member object
* @return {void}
*/
HouseholdBuilderController.prototype.onInvalidHouseholdMember = function onInvalidHouseholdMember(householdMember) {
this.householdBuilderView.displayValidationHints(householdMember);
};
/**
* Delegate requests from the UI to add a household member.
*
* @memberof HouseholdBuilderController
* @instance
* @param {HouseholdMember} householdMember the household member to add
* @return {void}
*/
HouseholdBuilderController.prototype.handleAddHouseholdMember = function handleAddHouseholdMember(householdMember) {
if (this.household.addHouseholdMember(householdMember)) {
this.householdBuilderView.resetForm();
} else {
this.onInvalidHouseholdMember(householdMember);
}
};
/**
* Delegate requests from the UI to remove a household member.
*
* @memberof HouseholdBuilderController
* @instance
* @param {number} id the id of the household member to remove
* @return {void}
*/
HouseholdBuilderController.prototype.handleRemoveHouseholdMember = function handleRemoveHouseholdMember(id) {
this.household.removeHouseholdMember(id);
};
/**
* Delegate requests from the UI to submit a household.
*
* @memberof HouseholdBuilderController
* @instance
* @return {void}
*/
HouseholdBuilderController.prototype.handleSubmitHousehold = function handleSubmitHousehold() {
var serializedHousehold = this.household.serializeHouseholdMemberData();
this.householdBuilderView.displaySerializedHousehold(serializedHousehold);
};
/**
* Main Application Flow
*/
document.addEventListener('DOMContentLoaded', function startApp() {
var builder = new HouseholdBuilderController(
new HouseholdBuilderView(),
new Household()
);
// Initialize the builder
builder.start();
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment