-
Notifications
You must be signed in to change notification settings - Fork 7
Add dynamic content and manage state with vanilla Javascript
Now we will implement a lot of code for handling the workflow. This is unfortunately a large example, we do a lot of stuff here. But I have tried to break it down to make it easier to follow.
First up is a function for showing, hiding and appending elements to the grey background for modals.
const GreyModalElement = function() {
const element = document.getElementById('grey-modal-background');
return {
show: function() {
element.classList.remove('hidden');
},
hide: function() {
element.innerHTML = "";
element.classList.add('hidden');
},
appendChild: function(childElement) {
element.appendChild(childElement);
}
}
}();
It is not a class as we only need one "instance". Also called a singleton.
Next up are the form classes, we will create 4 of them. This may look like an awful amount of code, but it is quite simple actually. I will try to explain their purpose.
-
The first class is BaseFormAbstract, it is an abstract class, you cannot instantiate it and it only serves one purpose: building the FORM element. It can be used for any types of forms you will create.
-
Next class is BookFormAbstract which extends the BaseFormAbstract class. Another abstract class that you never want to instantiate directly. This class constructs a default book form that you can build on with further class extending. It also creates the cancel and submit button elements with eventlisteners for the form. When you submit the form it will also run form validation, which is nice when you don't want the end-user to submit empty fields. But this class still use form submit, and we are developing a javascript example with no backend.
-
The CreateBookFormComponent class is the form we create when we want to add a book to our table. We have all the behavior we want in the BookFormAbstract class, except for the submit method. So the only thing different with this class is the submit method. This is an excellent way to show good code reuse by extending classes.
-
Finally we have the EditBookFormComponent class, it also extends the BookFormAbstract class. When editing a book, we need to bind book data to the form. So it has a bindBookToForm() method for that. We also need a id field that will work as a reference to which book in the BooksCollection class we are going to update.
/**
* Abstract class BaseForm
* This class contains business logic for the form and the submit button.
*/
class BaseFormAbstract{
constructor(obj){
if (new.target === BaseFormAbstract) {
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();
}
}
/**
* Abstract class BookFormAbstract
* This class contains business logic for the book form.
*/
class BookFormAbstract extends BaseFormAbstract{
constructor(obj){
super(obj);
if (new.target === BookFormAbstract) {
throw new TypeError("Cannot construct Abstract instances directly");
}
this.destroyFormFn = () => {
GreyModalElement.hide();
};
this.submitEventFn = event => {
event.preventDefault();
this.submit();
};
this.updateFormElement();
this.updateSubmitButtonElement();
this.buildCancelButton();
}
updateFormElement() {
this.formElement.className = "modal";
}
updateSubmitButtonElement() {
this.submitButtonElement.textContent = "Add book";
this.submitButtonElement.classList.add('green');
this.submitButtonElement.addEventListener('click', this.submitEventFn);
}
buildCancelButton(){
this.cancelButtonElement = document.createElement('BUTTON');
this.cancelButtonElement.textContent = "Cancel";
this.cancelButtonElement.addEventListener('click', 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;
// Add error class to input field
if(!formFields[i].classList.contains('error')){
formFields[i].classList.add('error');
}
} else {
// If no error, remove error class if exists
if(formFields[i].classList.contains('error')){
formFields[i].classList.remove('error');
}
}
}
return isValid;
}
submit(){
if(this.validate()){
this.formElement.submit();
}
}
render(){
this.formElement.innerHTML = `
<label>Title:</label>
<input type="text" name="title" value="" tabindex="10"/>
<label>ISBN:</label>
<input type="text" name="isbn" value="" tabindex="20"/>
<label>Author:</label>
<input type="text" name="author.name" value="" tabindex="30"/>
`;
this.cancelButtonElement.tabIndex = 100;
this.submitButtonElement.tabIndex = 110;
this.formElement.appendChild(this.submitButtonElement);
this.formElement.appendChild(this.cancelButtonElement);
// assign input elements to this object,
// so it is easier to reference them in code
this.inputFieldForTitle =
this.formElement.querySelector('input[name="title"]');
this.inputFieldForAuthor =
this.formElement.querySelector('input[name="author.name"]');
this.inputFieldForISBN =
this.formElement.querySelector('input[name="isbn"]');
}
}
/**
* Create book form component
* This class contains business logic creating a book.
*/
class CreateBookFormComponent extends BookFormAbstract{
constructor(obj, booksCollection){
super(obj);
this.booksCollection = booksCollection;
}
submit(){
if(this.validate()){
let book = new BookModel({
id: this.booksCollection.books.length + 1,
title: this.inputFieldForTitle.value,
author: new AuthorModel({
name: this.inputFieldForAuthor.value
}),
isbn: this.inputFieldForISBN.value
});
this.booksCollection.addBook(book);
this.destroyFormFn();
}
}
}
/**
* Create book form component
* This class contains business logic updating a book.
*/
class EditBookFormComponent extends BookFormAbstract{
constructor(obj, booksCollection, book){
super(obj);
this.booksCollection = booksCollection;
this.book = book;
this.submitButtonElement.textContent = "Update book";
this.buildInputFieldForId();
}
buildInputFieldForId(){
this.inputFieldForId = document.createElement('INPUT');
this.inputFieldForId.name = "id";
this.formElement.appendChild(this.inputFieldForId);
}
bindBookToForm() {
this.inputFieldForTitle.value = this.book.title;
this.inputFieldForAuthor.value = this.book.author.name;
this.inputFieldForISBN.value = this.book.isbn;
this.inputFieldForId.value = this.book.id;
}
submit(){
if(this.validate()){
const bookProperties = {
id: Number(this.inputFieldForId.value),
title: this.inputFieldForTitle.value,
// I just create a new author here for the sake of simplicity
author: new AuthorModel({
name: this.inputFieldForAuthor.value
}),
isbn: this.inputFieldForISBN.value
};
this.booksCollection.updateBook(bookProperties);
this.destroyFormFn();
}
}
render(){
super.render();
this.bindBookToForm();
}
}
We are getting close to having a working example. Next step is updating our BookTableComponent class:
/**
* Book table component. We call this a component as its behaviour is a
* reusable component for web composition.
*
* With this design it is also easier to map it over to a true web-component,
* which will hopefully soon become a standard in all the major browsers.
*/
class BookTableComponent{
constructor(obj){
this.containerElement = obj.containerElement;
this.fields = BookModel.getFields();
this.updateProperties(obj);
this.showCreateBookModalFn = () => {
const createBookForm =
new CreateBookFormComponent({}, this.booksCollection);
createBookForm.render();
GreyModalElement.appendChild(createBookForm.formElement);
GreyModalElement.show();
createBookForm.inputFieldForTitle.focus();
};
this.showEditBookModalFn = event => {
// This make sure that we always get the row element even if we
// click the td element
const rowElement = event.target.closest('tr');
const bookId =
Number(rowElement.querySelector('td:first-child').textContent);
const editBookForm =
new EditBookFormComponent(
{},
this.booksCollection,
this.booksCollection.getBook(bookId)
);
editBookForm.render();
GreyModalElement.show();
GreyModalElement.appendChild(editBookForm.formElement);
editBookForm.inputFieldForTitle.focus();
};
this.buildDOMElements();
this.render();
}
updateProperties(obj) {
this.booksCollection = new BookCollectionModel(obj.books);
this.booksCollection.subscribe(()=>{
this.render();
});
}
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.createAddBookBtn();
}
createAddBookBtn(){
// Add book button element
this.createBookBtnElement = document.createElement('BUTTON');
this.createBookBtnElement.textContent = "Create Book";
this.createBookBtnElement
.addEventListener('click', this.showCreateBookModalFn);
}
renderHead(){
// map() will loop the fields property and create the <th> elements
this.tableHeaderElement.innerHTML = `
<tr>
${this.fields.map(item => `<th>${item}</th>`).join('')}
</tr>
`;
}
renderBody(){
this.tableBodyElement.innerHTML = `
${this.booksCollection.books.map(book => `
<tr>
<td>${book.id}</td>
<td>${book.title}</td>
<td>${book.isbn}</td>
<td>${book.author.name}</td>
</tr>
`).join('')}
`;
for(let i = 0; i < this.tableBodyElement.children.length; i++){
const rowElement = this.tableBodyElement.children[i];
rowElement.addEventListener('click', this.showEditBookModalFn);
}
}
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);
}
}
A lot of things has been added, BookTableComponent class now have events for showing both the add and edit book form modal. But the most interesting part is that we are now subscribing for events when our BooksCollectionModel class gets a new book or an updated book.
The code is very simple, we just create a function this will invoke the BookTableComponent render() method when there is a new book or when you update a book:
// inside the BookTableComponent class
updateProperties(obj) {
this.booksCollection = new BooksCollection(obj);
this.booksCollection.subscribe(()=>{
this.render();
});
}
Thats it! Now you should see your new table with the functionality you just implemented
The example3 folder contains the full source code for a working example.