Created
March 26, 2020 16:01
-
-
Save loranallensmith/3f4d29c8d025ba24590b7f604bd77fd9 to your computer and use it in GitHub Desktop.
JavaScript Example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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