Skip to content

Add dynamic content and manage state with vanilla Javascript

Olav Grønås Gjerde edited this page Apr 15, 2018 · 21 revisions

So we have come so far to implement a table, and now we want to add manipulations to it.

Adding more infrastructure

Traits

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);

};

Refactoring books reference

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]);
		}
	}

Implementing the add book button

Lets start with the simple stuff first. Adding an add new book button.

Create the button element

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: books table with btn

Create the eventlistener

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.