Check the lab 7 post to see how to get the html and css files.

Each section represents one of the functions and in some cases the functions are connected to each other. To see how everything ties up check the last section.

Toggling the form visibility

To do this, we have to toggle the class active on the form container. Also, don't forget to add the event listener.

function toggleFormVisibility() {
    document.querySelector('#form--container').classList.toggle('active')
}

document.querySelector('#form_toggle').addEventListener('click', toggleFormVisibility)

Clear the form inputs

We can do this by setting the value of the inputs to an empty string.

function clearFormInputs() {
    document.querySelector('#firstname').value = ''
    document.querySelector('#lastname').value = ''
    document.querySelector('#phonenumber').value = ''
    document.querySelector('#index').value = ''
}

Checking if the inputs are valid

The form needs to be validated meaning that we do not allow numbers or special characters in neither the first, nor last name and the phone number shall only allow digits 0-9, +, and ().

For more complex validation we have to use Regular Expressions (RegEx). RegEx are used to define search patterns and they might look weird but learning how to use them can be very useful even in areas other than programming. If you don't want to do that you can just Google for them just like I did.

function formIsValid() {
    return /^[a-zA-Z]*$/.test(document.querySelector('#firstname').value) && 
        /^[a-zA-Z]*$/.test(document.querySelector('#lastname').value) &&
        /^[+]*[(]{0,1}[0-9]{1,4}[)]{0,1}[-\s\./0-9]*$/.test(document.querySelector('#phonenumber').value)
}

Let's go over what is happening in the above code block.

/^[a-zA-Z]*$/.test(document.querySelector('#firstname').value) - here we are checking if the value inside of the first name input matches our RegEx. This expression only allows for the letter a-z and A-Z. If the value inside our input matches this pattern (basically, is valid) then the test function will return true.

If all of our checks are good and we get back true from all of them then our formIsValid will also return true.

Getting all the contacts

We are storing all of our contacts inside of the local storage so in order to add, remove or edit a contact we first have to retrieve the list of contacts.

function getAllContacts() {
    return JSON.parse(localStorage.getItem('contacts')) || []
}

Here I used a little bit of Javascript trickery but in order to understand it let me first show you the equivalent by using an if statement.

if (JSON.parse(localStorage.getItem('contacts)) {
  return JSON.parse(localStorage.getItem('contacts')
}
else {
  return []
}

If we already have the contacts in our local storage we get them, if not we just return an empty array.

return JSON.parse(localStorage.getItem('contacts')) || [] - This works because of how Javascript evaluates the logical operators (||, &&). When we use the or logical operator or only one of the checks needs to be true for the check to pass. In our case if JSON.parse(localStorage.getItem('contacts') has a value then the or will be evaluated to true so this will be returned. In the case when the contacts don't exist it will also go on to the second part of the logical operation and [] will be returned.

This is called a short-circuit evaluation and you can read more about it here.

Adding a contact

Let's start with adding a contact.

function addContact() {
    if (!formIsValid()) {
        alert('oopsie :(')
        return
    }

    if (index = document.querySelector('#index').value) {
        editContact(index)
        return
    }

    const contacts = getAllContacts()
    contacts.push({
        firstName: document.querySelector('#firstname').value,
        lastName: document.querySelector('#lastname').value,
        phoneNumber: document.querySelector('#phonenumber').value
    })

    updateContacts(contacts)

    toggleFormVisibility()
    clearFormInputs()
}

document.querySelector('#add_new--button').addEventListener('click', addContact)

We are first using our input validating function to check if the user's input follows our guidelines and in case it isn't the function just stops.

The second if statement is used for the edit part and we'll get into that a little bit later.

After we have ensured the input is correct the user actually intends to add a contact we are retrieving the contact list and adding a new contact to it by pushing a new object with the values we got from the inputs.

In the end we update the contact list so it also includes the new contact and we hide the form and clear the inputs by using the functions we went over in the first sections.

Updating the contacts

Near the end of the add contact function we have used the updateContacts function so let's see what that does.

function updateContacts(contacts) {
    localStorage.setItem('contacts', JSON.stringify(contacts))

    renderContacts()
}

The function receives the contacts array as a parameter and takes that array and puts it into the local storage in a JSON format. You might have also notice above in the get all contacts function that we use JSON.parse. This is because local storage is limited and arrays cannot be put in there. To get over this limitation we have to turn our contacts array into JSON format.

After the contacts are updated in the local storage we go ahead and call the renderContacts function that will display the updated contacts in the HTML.

Displaying the contacts

function renderContacts() {
    const contactList = document.querySelector('#contacts--result')
    contactList.innerHTML = ''
    
    getAllContacts().forEach( (contact, index) => {
        const contactContainer = document.createElement('div')
        contactContainer.classList.add('contact--item')

        const name = document.createElement('h3')
        name.textContent = `${contact.firstName} ${contact.lastName}`
        contactContainer.appendChild(name)

        const phoneNumber = document.createElement('p')
        phoneNumber.textContent = contact.phoneNumber
        contactContainer.appendChild(phoneNumber)

        const editButton = document.createElement('button')
        editButton.textContent = 'Edit'
        editButton.classList.add('edit-button')
        editButton.dataset.id = index
        editButton.addEventListener('click', showEditForm)
        contactContainer.appendChild(editButton)

        const deleteButton = document.createElement('button')
        deleteButton.textContent = 'Delete'
        deleteButton.classList.add('delete-button')
        deleteButton.dataset.id = index
        deleteButton.addEventListener('click', deleteContact)
        contactContainer.appendChild(deleteButton)

        contactList.appendChild(contactContainer)
    })
}

Quite the chonker. Let's go over what it does.

We first need to select the container in which our contacts will be displayed and clear it.

What is getAllContacts().forEach() and that => stuff supposed to mean??? Don't worry about it, let's turn it into a normal for loop.

const contacts = getAllContact()
for (let index = 0; index < contacts.length; index++) {
  const contact = contacts[index]
  // rest of the code
}

Yes I used index instead of the classic i. Don't panic, this is so it doesn't mess up the rest of the code inside the for.

Let's talk about what happens in this for loop.

We first have to create a container to put all our contact information and buttons into and add the class of contact--item to it.

After that we have to create a new element for our name.

name.textContent = `${contact.firstName} ${contact.lastName}`

Here we put put the first name and the last name together and then into the newly created name element. The equivalent of this is name.textContent = contact.firstName + ' ' + contact.lastName. The syntax I used above is called template literals and I strongly recommend to read more about it here.

Now that this name element is finished we have to attach it to the contact container by using appendChild.

The same process is applied for creating the phone number element so I'm not gonna get into it.

Lastly we have to add the edit and delete buttons.

Everything is pretty clear here, we create the buttons, put the text inside them, add classes and add event listeners so they actually do something. I'm going to go over the functions inside the event listeners after this section.

The weird part here is this deleteButton.dataset.id = index. By doing this we are attaching a reference to the button so we know what contact to delete when we press the button. To learn more about *data attributes** and the dataset i urge you to read this.

Removing a contact

function deleteContact() {
    const contacts = getAllContacts()
    contacts.splice(this.dataset.id, 1)

    updateContacts(contacts)
}

All that we have to do here is get all the contacts, delete the contact at the index we get from the pressed button and then display the contacts again. If this seems confusing check the end of the previous section again.

Editing a contact

function showEditForm() {
    document.querySelector('#index').value = this.dataset.id

    const contact = getAllContacts()[this.dataset.id]

    document.querySelector('#add_new--button').textContent = 'Edit contact'

    document.querySelector('#firstname').value = contact.firstName
    document.querySelector('#lastname').value = contact.lastName
    document.querySelector('#phonenumber').value = contact.phoneNumber

    if (!document.querySelector('#form--container').classList.contains('active')) {
        toggleFormVisibility()
    }
}

When the edit button is pressed on a contact there is a hidden element inside the form that we have to fill in with that contact's id (index) so the next time we press the previously named add contact button it will edit the selected contact and not add a new one.

Confusing, I know. Let's see what the function does.

First, the hidden input is filled with the contact's id.

After this we get the contact from our contacts array by using the id (which is also the index of the contact).

We have to change the text inside of the add contact button because this button will also be used to finish editing.

We also have to fill in all of the inputs with the information from the contact we just pressed edit on.

Last thing, when you click edit on a contact you need to toggle the form's visibility, but if the form is already open you don't want to do that so that is why the last if statement is there.

Edit contact

function addContact() {
    ...

    if (index = document.querySelector('#index').value) {
        editContact(index)
        return
    }
   
    ...
}

Remember this in the add contact form? Because we have set a value to that hidden input now the add contact function will lead to the edit one. Magical.

function editContact(index) {
    const contacts = getAllContacts()
    contacts[index] = {
        firstName: document.querySelector('#firstname').value,
        lastName: document.querySelector('#lastname').value,
        phoneNumber: document.querySelector('#phonenumber').value
    }

    updateContacts(contacts)

    toggleFormVisibility()
    clearFormInputs()

    document.querySelector('#add_new--button').textContent = 'Add new contact'
}

We get the index as a parameter so all we have to do is get all the contacts and modify the contact at that specific index using the value from the form inputs. After this, update the contacts, toggle the visibility of the form, clear the inputs and don't forget to change the text inside the button back.

Displaying the contacts when we open the website

document.addEventListener('DOMContentLoaded', renderContacts)

The DOMContentLoaded event is called your HTML is fully loaded and that is a great moment for us to display the contacts.

And we're done. Good job. I guess.

Everything put together

function toggleFormVisibility() {
    document.querySelector('#form--container').classList.toggle('active')
}

function clearFormInputs() {
    document.querySelector('#firstname').value = ''
    document.querySelector('#lastname').value = ''
    document.querySelector('#phonenumber').value = ''
    document.querySelector('#index').value = ''
}

function formIsValid() {
    return /^[a-zA-Z]*$/.test(document.querySelector('#firstname').value) && 
        /^[a-zA-Z]*$/.test(document.querySelector('#lastname').value) &&
        /^[+]*[(]{0,1}[0-9]{1,4}[)]{0,1}[-\s\./0-9]*$/.test(document.querySelector('#phonenumber').value)
}

function getAllContacts() {
    return JSON.parse(localStorage.getItem('contacts')) || []
}

function renderContacts() {
    const contactList = document.querySelector('#contacts--result')
    contactList.innerHTML = ''
    
    getAllContacts().forEach( (contact, index) => {
        const contactContainer = document.createElement('div')
        contactContainer.classList.add('contact--item')

        const name = document.createElement('h3')
        name.textContent = `${contact.firstName} ${contact.lastName}`
        contactContainer.appendChild(name)

        const phoneNumber = document.createElement('p')
        phoneNumber.textContent = contact.phoneNumber
        contactContainer.appendChild(phoneNumber)

        const editButton = document.createElement('button')
        editButton.textContent = 'Edit'
        editButton.classList.add('edit-button')
        editButton.dataset.id = index
        editButton.addEventListener('click', showEditForm)
        contactContainer.appendChild(editButton)

        const deleteButton = document.createElement('button')
        deleteButton.textContent = 'Delete'
        deleteButton.classList.add('delete-button')
        deleteButton.dataset.id = index
        deleteButton.addEventListener('click', deleteContact)
        contactContainer.appendChild(deleteButton)

        contactList.appendChild(contactContainer)
    })
}

function updateContacts(contacts) {
    localStorage.setItem('contacts', JSON.stringify(contacts))

    renderContacts()
}

function addContact() {
    if (!formIsValid()) {
        alert('oopsie :(')
        return
    }

    if (index = document.querySelector('#index').value) {
        editContact(index)
        return
    }

    const contacts = getAllContacts()
    contacts.push({
        firstName: document.querySelector('#firstname').value,
        lastName: document.querySelector('#lastname').value,
        phoneNumber: document.querySelector('#phonenumber').value
    })

    updateContacts(contacts)

    toggleFormVisibility()
    clearFormInputs()
}

function deleteContact() {
    const contacts = getAllContacts()
    contacts.splice(this.dataset.id, 1)

    updateContacts(contacts)
}

function showEditForm() {
    document.querySelector('#index').value = this.dataset.id

    const contact = getAllContacts()[this.dataset.id]

    document.querySelector('#add_new--button').textContent = 'Edit contact'

    document.querySelector('#firstname').value = contact.firstName
    document.querySelector('#lastname').value = contact.lastName
    document.querySelector('#phonenumber').value = contact.phoneNumber

    if (!document.querySelector('#form--container').classList.contains('active')) {
        toggleFormVisibility()
    }
}

function editContact(index) {
    const contacts = getAllContacts()
    contacts[index] = {
        firstName: document.querySelector('#firstname').value,
        lastName: document.querySelector('#lastname').value,
        phoneNumber: document.querySelector('#phonenumber').value
    }

    updateContacts(contacts)

    toggleFormVisibility()
    clearFormInputs()

    document.querySelector('#add_new--button').textContent = 'Add new contact'
}

document.querySelector('#form_toggle').addEventListener('click', toggleFormVisibility)
document.querySelector('#add_new--button').addEventListener('click', addContact)
document.addEventListener('DOMContentLoaded', renderContacts)