-
Notifications
You must be signed in to change notification settings - Fork 7
Add dynamic content and manage state with vanilla Javascript
So we have come so far to implement a table, and now we want to add manipulations to it.
I will not go much into what traits is, you can search the web for more information. Basically a Trait is a structual construct which makes it easier to create class behavior without repeating yourself. It is similar to mixins, just that it you don't need to add it to every instance. There is a good stack overflow post for a simple Trait implementation in Javascript:
function Trait (methods) {
this.traits = [methods];
}
// https://stackoverflow.com/questions/1978770/traits-in-javascript
Trait.prototype = {
constructor: Trait,
uses: function (trait) {
this.traits = this.traits.concat(trait.traits);
return this;
},
useBy: function (obj) {
for(let i = 0; i < this.traits.length; ++i) {
let methods = this.traits[i];
for (let prop in methods) {
if (methods.hasOwnProperty(prop)) {
obj[prop] = obj[prop] || methods[prop];
}
}
}
}
};
Trait.unimplemented = function (obj, traitName) {
if (obj === undefined || traitName === undefined) {
throw new Error ("Unimplemented trait property.");
}
throw new Error (traitName + " is not implemented for " + obj);
};
We want to change the books list reference as we want to create listeners when we add/update/delete books.
For this we create a BooksCollections class which has to be added before the BooksTable class.
class BooksCollection{
constructor(){
this.books = [];
}
getBook(id){
for(let i = 0; i < this.books.length; i++){
if(this.books[i].id === id){
return this.books[i];
}
}
}
addBook(book){
this.books.push(book);
}
deleteBook(book){
for(let i = 0; i < this.books.length; i++){
if(this.books[i] === book){
this.books.splice(i, 1);
break;
}
}
}
updateBook(obj){
const book = this.getBook(obj.id);
book.updateProperties(obj);
}
}
We also update the updateProperties method in BooksTable to:
updateProperties(obj) {
this.booksCollection = new BooksCollection();
for(let i = 0; i < obj.books.length; i++){
this.booksCollection.addBook(obj.books[i]);
}
}
Lets start with the simple stuff first. Adding an add new book button.
In the BooksTable class in the buildDOMElements() method we add the code for the button:
buildDOMElements() {
this.tableElement = document.createElement('TABLE');
this.tableHeaderElement = this.tableElement.createTHead();
// There is no createTBody function
this.tableBodyElement = document.createElement('TBODY');
this.tableElement.appendChild(this.tableBodyElement);
// Add book button element
this.createBookBtnElement = document.createElement('BUTTON');
this.createBookBtnElement.textContent = "Create Book";
}
We also need to add it to the render() method:
render(){
this.renderHead();
this.renderBody();
this.containerElement.innerHTML = "";
this.containerElement.appendChild(this.tableElement);
// Append add book button element to the container
this.containerElement.appendChild(this.createBookBtnElement);
}
Thats it! Now you should see the book button when you refresh:
But we also need to implement functionality for the button. It needs an eventlistener
Lets start with some code refactoring where we move the creation of add book button element to its own method.
buildDOMElements() {
this.tableElement = document.createElement('TABLE');
this.tableHeaderElement = this.tableElement.createTHead();
// There is no createTBody function
this.tableBodyElement = document.createElement('TBODY');
this.tableElement.appendChild(this.tableBodyElement);
this.buildAddBookBtn();
}
buildAddBookBtn(){
// Add book button element
this.createBookBtnElement = document.createElement('BUTTON');
this.createBookBtnElement.textContent = "Create Book";
const showCreateBookModalFn = () => {
const modalContainerEle =
document.getElementById('grey-modal-background');
modalContainerEle.classList.remove('hidden');
const createBookForm =
new CreateBookFormForTable({}, this.booksCollection);
createBookForm.render();
modalContainerEle.appendChild(createBookForm.formElement);
};
this.createBookBtnElement
.addEventListener('click', showCreateBookModalFn);
}
Now we have a function that displays a grey background layer over the whole web page and create an instance of the CreateBookForTable class. So what is that class? We need to implement the code for it:
We start with a base form class, BaseFormAbstract
class BaseFormAbstract{
constructor(obj){
if (new.target === BaseForm) {
throw new TypeError("Cannot construct Abstract instances directly");
}
this.method = obj.method || 'POST';
this.action = obj.action || null;
this.enctype = obj.enctype || null;
this.buildDOMElements();
this.updateAttributes();
}
buildDOMElements() {
this.formElement = document.createElement('FORM');
this.submitButtonElement = document.createElement('BUTTON');
this.submitButtonElement.type = "submit";
this.submitButtonElement.textContent = 'Submit';
}
updateAttributes() {
this.formElement.method = this.method;
this.formElement.action = this.action;
if(this.enctype){
this.formElement.enctype = this.enctype;
}
}
submit(){
this.formElement.submit();
}
}
This is an abstract class, which means that you cannot instantiated it. It only handles the logic for the
element.Next up is the actual form with fields:
class CreateBookForm extends BaseFormAbstract{
constructor(obj){
super(obj);
this.destroyFormFn = () => {
const modalContainerEle =
document.getElementById("grey-modal-background");
modalContainerEle.innerHTML = "";
modalContainerEle.classList.add('hidden');
};
this.submitEventFn = e => {
e.preventDefault();
this.submit();
};
this.updateFormElement();
this.updateSubmitButtonElement();
this.buildCancelButton();
}
updateFormElement() {
this.formElement.name = "create-book";
this.formElement.className = "modal";
}
updateSubmitButtonElement() {
this.submitButtonElement.textContent = "Add book";
this.submitButtonElement.classList.add('green');
this.submitButtonElement.addEventListener('click', this.submitEventFn);
this.submitButtonElement.addEventListener('keypress', e => {
if(document.activeElement === e.target){
this.submitEventFn(e);
}
});
}
buildCancelButton(){
this.cancelButtonElement = document.createElement('BUTTON');
this.cancelButtonElement.textContent = "Cancel";
this.cancelButtonElement.addEventListener('click', this.destroyFormFn);
this.cancelButtonElement.addEventListener('keypress', e => {
if(document.activeElement === e.target){
this.destroyFormFn();
}
});
}
validate(){
let isValid = true;
const formFields =
this.formElement.querySelectorAll('input[type="text"]');
for(let i = 0; i < formFields.length; i++){
if(formFields[i].value === ""){
isValid = false;
if(!formFields[i].classList.contains('error')){
formFields[i].classList.add('error');
}
} else {
if(formFields[i].classList.contains('error')){
formFields[i].classList.remove('error');
}
}
}
return isValid;
}
render(){
this.formElement.innerHTML = `
<label>Title:</label>
<input type="text" name="title" value=""/>
<label>ISBN:</label>
<input type="text" name="isbn" value=""/>
<label>Author:</label>
<input type="text" name="author.name" value=""/>
`;
this.formElement.appendChild(this.submitButtonElement);
this.formElement.appendChild(this.cancelButtonElement);
}
}
This class implements the fields, buttons and field validation. But this is not the CreateBookFormForTable class either. The CreateBookFormForTable class is a subclass of the CreateBookForm class:
class CreateBookFormForTable extends CreateBookForm{
constructor(obj, booksCollection){
super(obj);
this.booksCollection = booksCollection;
}
submit(){
if(this.validate()){
let book = new Book({
id: this.booksCollection.length + 2,
title: this.formElement.querySelector('input[name="title"]').value,
author: new Author({
name: this.formElement.querySelector('input[name="author.name"]').value
}),
isbn: this.formElement.querySelector('input[name="isbn"]').value
});
this.booksCollection.addBook(book);
this.destroyFormFn();
}
}
}
Ecmascript 6 does not support nesting of classes, so we cannot encapsulate this class inside the BooksTable class. But this class is only for the BooksTable. It also comes with an extra argument as we need to use the same booksCollection reference.