0

I made 'book' items using javascript in a div with the 'content' class and added an delete button on each of them dynamically so I can delete them. I'm trying to figure what what is wrong but I don't understand what I'm missing.

It seems to work as intended but in console it gives me an error that goes as following:

index.html:228 Uncaught TypeError: Failed to execute 'removeChild' on 'Node': 
parameter 1 is not of type 'Node'.
at HTMLButtonElement.<anonymous> (index.html:228:25)

but the item is removed successfully. What am I missing???

    //1. store the book objects in a simply array.
    const books = [];
    //2. add a function that takes the user's input. this function adds the object
    // in the array
    //3. write a function that loops through the array
    //and displays each book on the page in a card.
    // //4. add a 'new book' button so user can add a new book.
    const newBook = document.getElementById('new-book');
    const form = document.querySelector('form');
    const body = document.querySelector('body');
    const formButton = document.getElementById('form-button');
    const content = document.querySelector('.content');

    newBook.addEventListener('click', e => {
        toggleForm();
    })

    function toggleForm() {
        if (form.classList.value === 'form-hidden') {
            form.className = 'form-visible';
        }
        else {
            form.className = 'form-hidden';
        }
    }

    formButton.addEventListener('click', e => {
        const obj = {};
        const inputs = Array.from(document.querySelectorAll('input'));
        inputs.forEach(input => {
            obj[input.name] = input.value;
        })
        books.push(obj);
        inputs.forEach(input => {
            input.value = '';
        })
        toggleForm();
        createBooks();
        const deleteBtns = Array.from(document.querySelectorAll('#delete-btn'));
        deleteBtns.forEach(btn => {
            btn.addEventListener('click', e => {
                const btnDataKey = parseInt(btn.getAttribute('data-key'));
                content.removeChild(document.getElementById(btnDataKey));
            })
        })
    })

    function createBooks() {
        const bookDiv = document.createElement('div');
        bookDiv.className = 'book';
        const bookObj = books.length-1;
        for (const key in books[bookObj]) {
            const bookGrid = document.createElement('div');
            bookGrid.className = 'book-grid';
            const h3 = document.createElement('h1');
            h3.textContent = `${key}:`;
            const p = document.createElement('h2');
            p.textContent = books[bookObj][key];
            h3.className = 'long';
            p.className = 'long';
            bookGrid.append(h3, p);
            bookDiv.appendChild(bookGrid);
            const btn = document.createElement('button');
            btn.style.top = '0.3rem';
            btn.style.right = '.5rem';  
            btn.style.position= 'absolute';
            btn.textContent = 'Delete';
            btn.className = 'button-1';
            btn.style.fontSize = '1rem';
            btn.setAttribute('id', 'delete-btn');
            btn.setAttribute('data-key', `${bookObj}`);
            // const number = btn.getAttribute('data-key');
            // console.log(number);
            bookDiv.appendChild(btn);
        }
        bookDiv.setAttribute('id', `${bookObj}`);
        console.log (`this is the id of bookDiv ${bookDiv.id}`);
        bookDiv.style.position='relative';
        content.appendChild(bookDiv);
    }
* {
        padding: 0px;
        margin: 0px;
        font-family: 'ComiliBook', Arial, Helvetica, sans-serif;
        font-weight: normal;
        font-style: normal;
    }

    body {
        min-height: 100vh;
        display: grid;
        grid-template-rows: 1fr 6fr 1fr;
        overflow: auto;
    }

    .nav,
    .main,
    .footer {
        display: flex;
        justify-content: center;
        align-items: center;
        background-color: black;
        color: white;
        font-size: 2.5rem;
    }

    .main {
        background-color: white;
        color: black;
        font-size: 1rem;
        padding: 3.5rem;
    }

    .main>.content {
        width: 100%;
        height: 100%;
        border: solid 1px black;
        border-radius: 10px;
        position: relative;
        display: grid;
        margin: -2rem;
        padding: 2rem;
        gap: 1rem;
        grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
        background-color: RGB(240, 240, 240);
    }

    .form-container {
        padding: 1rem;
        position: absolute;
        top: .1rem;
        left: .1rem;
        z-index: 1;
    }

    .button-1,
    .button-2 {
        padding: .2rem .5rem;
        font-size: 1.5rem;
        background-color: white;
        color: black;
        border: 1px solid black;
        border-radius: 10px;
    }

    .button-2 {
        font-size: 1rem;
    }

    .button-1:hover,
    .button-2:hover {
        background-color: black;
        color: white;
    }

    .button-1:active,
    .button-2:active {
        background-color: black;
        color: white;
        transform: translate(0px, .2rem);
    }

    form {
        border: solid 1px black;
        position: absolute;
        padding: 1rem;
        display: grid;
        gap: .5rem;
        border-radius: 10px;
        background-color: black;
        color: white;
        font-size: 1.3rem;
    }

    .form-hidden {
        visibility: hidden;
    }

    .form-visible {
        visibility: visible;
    }

    .book {
        border: 1px solid black;
        padding: 1rem;
        display: grid;
        grid-template-rows: repeat(auto, 1fr);
        border-radius: 10px;
        align-items: center;
        background-color: gray;
        gap: 1rem;
    }

    .book-grid {
        display: grid;
        grid-template-columns: 1fr 2fr;
        border-radius: 10px;
        padding: 0.5rem;
        background-color: white;
    }

    h1, h2 {
        font-size: 1rem;
        text-overflow: ellipsis;
        font-family: Arial, Helvetica, sans-serif;
    }

    h2 {
        text-align: start;
    }

    .long {
        text-overflow: ellipsis;
        white-space: nowrap;
        overflow: hidden;
    }

    .delete-btn {
        top: 1rem;
        left: 1rem;
    }
<!DOCTYPE html>
<html lang="en">

<head>
    <link rel="stylesheet" media="screen" href="https://fontlibrary.org//face/comili" type="text/css" />
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <div class="nav">My Home Library</div>
    <div class="main">
        <div class="content">
            <div class="form-container">
                <button id="new-book" class="button-1">New Book</button>
                <form action="#" method="post" class='form-hidden'>
                    <div>
                        <label for="author">Author:</label>
                        <input type="text" name="Author" id="author">
                    </div>
                    <div>
                        <label for="title">Title:</label>
                        <input type="text" name="Title" id="title">
                    </div>
                    <div>
                        <label for="pages">Number of pages:</label>
                        <input type="number" name="Pages" id="pages">
                    </div>
                    <div>
                        <label for="status">Have I read the book?: </label>
                        <input type="text" name="Status" id="status">
                    </div>
                    <button class="button-2" type="button" id="form-button">Add a new book</button>
                </form>
            </div>
        </div>
    </div>
    <div class="footer">footer</div>
</body>
</html>
  • 1
    No such error occurs in the code you've shared here. – Scott Marcus Jul 05 '22 at 21:08
  • The above code snippet won't reproduce the error on my machine (Chrome web browser). The issue may be a timing problem with your JavaScript. Have you ensured that your page is loaded before the JavaScript runs? https://stackoverflow.com/q/520812/3196753 – tresf Jul 05 '22 at 21:08
  • Why do you `parseInt` the id? It should be string anyway. For me it solved the issue for some reason. – IT goldman Jul 05 '22 at 21:10
  • 1
    @tresf Since the code for removing a book isn't triggered until the user clicks the button, it's very likely that the code has been loaded by the time that happens. – Scott Marcus Jul 05 '22 at 21:10
  • @ScottMarcus agreed. In my case, the error was much different when due to order of operations as the call to `const newBook = document.getElementById('new-book');` halted all other code following. – tresf Jul 05 '22 at 21:12
  • Does document.getElementById(btnDataKey) return a Node in the first place? 2, can you remove a "grandchild" with removeChild? – Salman A Jul 05 '22 at 21:12
  • Given that the code you've posted here works as it should and doesn't throw the error you say you are getting, then the issue has to be something else. – Scott Marcus Jul 05 '22 at 21:13
  • Scott, I get the error when creating 2 books, and then deleting them. – trincot Jul 05 '22 at 21:15
  • Ok, the code calls `content.removeChild(document.getElementById(btnDataKey));`, so a non-valid HTML ID can cause some errors. Probably an exception thrown from previous testing as a result of not validating the field. I'll attempt to create a reproducible scenario and share an explaination. – tresf Jul 05 '22 at 21:16
  • The problem is that you are creating event listeners for all buttons at every click on the "add book" button. This means that when you add multiple books, the first delete buttons will have multiple click handlers, and although the first of those will work as intended, the other handlers on the same bitton will not be able to delete the same node, and so you get an error. – trincot Jul 05 '22 at 21:16
  • I do believe that the issue is related to the `id`s you are working with. You should avoid using `id`s in the first place as they create brittle code (as you can see from your issue). Instead, you can reference each dynamically added element by other means, such as its relative position within its parent. – Scott Marcus Jul 05 '22 at 21:25
  • @ScottMarcus yeah, they're adding their listeners multiple times, but to reproduce you need to add at least two books. Two solutions have been provided below. – tresf Jul 05 '22 at 21:39
  • A number of issues here but you are also generating invalid HTML with `btn.setAttribute('id', 'delete-btn');` – Mark Schultheiss Jul 05 '22 at 22:02

2 Answers2

2

The problem is that with every click on the "Add a new book" button, your code adds click listeners to all delete buttons, also those that already have them.

So once you have 2 or more books, the first delete buttons will have more than one click handler doing the same job. Only the first one that runs will do the job successfully. The others, trying to delete the book element again, will fail with an error.

So... don't add click handlers like that: remove that part of the code that loops over the buttons and adds click handlers. Instead add one listener on the content-element, and check if the click came from any of the delete buttons. If so, perform the deletion.

There is much to comment on your code, which I do not address here. For instance, HTML should never have multiple elements with the same ID. That is invalid. But I didn't touch any off that, so that you can see exactly what I changed to solve the problem you raised:

//1. store the book objects in a simply array.
    const books = [];
    //2. add a function that takes the user's input. this function adds the object
    // in the array
    //3. write a function that loops through the array
    //and displays each book on the page in a card.
    // //4. add a 'new book' button so user can add a new book.
    const newBook = document.getElementById('new-book');
    const form = document.querySelector('form');
    const body = document.querySelector('body');
    const formButton = document.getElementById('form-button');
    const content = document.querySelector('.content');

    newBook.addEventListener('click', e => {
        toggleForm();
    })

    function toggleForm() {
        if (form.classList.value === 'form-hidden') {
            form.className = 'form-visible';
        }
        else {
            form.className = 'form-hidden';
        }
    }
    
    content.addEventListener('click', e => {
        if (!e.target.textContent.includes("Delete")) return;
        const btnDataKey = e.target.getAttribute('data-key');
        content.removeChild(document.getElementById(btnDataKey));
    });

    formButton.addEventListener('click', e => {
        const obj = {};
        const inputs = Array.from(document.querySelectorAll('input'));
        inputs.forEach(input => {
            obj[input.name] = input.value;
        })
        books.push(obj);
        inputs.forEach(input => {
            input.value = '';
        })
        toggleForm();
        createBooks();
    })

    function createBooks() {
        const bookDiv = document.createElement('div');
        bookDiv.className = 'book';
        const bookObj = books.length-1;
        for (const key in books[bookObj]) {
            const bookGrid = document.createElement('div');
            bookGrid.className = 'book-grid';
            const h3 = document.createElement('h1');
            h3.textContent = `${key}:`;
            const p = document.createElement('h2');
            p.textContent = books[bookObj][key];
            h3.className = 'long';
            p.className = 'long';
            bookGrid.append(h3, p);
            bookDiv.appendChild(bookGrid);
            const btn = document.createElement('button');
            btn.style.top = '0.3rem';
            btn.style.right = '.5rem';  
            btn.style.position= 'absolute';
            btn.textContent = 'Delete';
            btn.className = 'button-1';
            btn.style.fontSize = '1rem';
            btn.setAttribute('id', 'delete-btn');
            btn.setAttribute('data-key', `${bookObj}`);
            // const number = btn.getAttribute('data-key');
            // console.log(number);
            bookDiv.appendChild(btn);
        }
        bookDiv.setAttribute('id', `${bookObj}`);
        console.log (`this is the id of bookDiv ${bookDiv.id}`);
        bookDiv.style.position='relative';
        content.appendChild(bookDiv);
    }
* {
        padding: 0px;
        margin: 0px;
        font-family: 'ComiliBook', Arial, Helvetica, sans-serif;
        font-weight: normal;
        font-style: normal;
    }

    body {
        min-height: 100vh;
        display: grid;
        grid-template-rows: 1fr 6fr 1fr;
        overflow: auto;
    }

    .nav,
    .main,
    .footer {
        display: flex;
        justify-content: center;
        align-items: center;
        background-color: black;
        color: white;
        font-size: 2.5rem;
    }

    .main {
        background-color: white;
        color: black;
        font-size: 1rem;
        padding: 3.5rem;
    }

    .main>.content {
        width: 100%;
        height: 100%;
        border: solid 1px black;
        border-radius: 10px;
        position: relative;
        display: grid;
        margin: -2rem;
        padding: 2rem;
        gap: 1rem;
        grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
        background-color: RGB(240, 240, 240);
    }

    .form-container {
        padding: 1rem;
        position: absolute;
        top: .1rem;
        left: .1rem;
        z-index: 1;
    }

    .button-1,
    .button-2 {
        padding: .2rem .5rem;
        font-size: 1.5rem;
        background-color: white;
        color: black;
        border: 1px solid black;
        border-radius: 10px;
    }

    .button-2 {
        font-size: 1rem;
    }

    .button-1:hover,
    .button-2:hover {
        background-color: black;
        color: white;
    }

    .button-1:active,
    .button-2:active {
        background-color: black;
        color: white;
        transform: translate(0px, .2rem);
    }

    form {
        border: solid 1px black;
        position: absolute;
        padding: 1rem;
        display: grid;
        gap: .5rem;
        border-radius: 10px;
        background-color: black;
        color: white;
        font-size: 1.3rem;
    }

    .form-hidden {
        visibility: hidden;
    }

    .form-visible {
        visibility: visible;
    }

    .book {
        border: 1px solid black;
        padding: 1rem;
        display: grid;
        grid-template-rows: repeat(auto, 1fr);
        border-radius: 10px;
        align-items: center;
        background-color: gray;
        gap: 1rem;
    }

    .book-grid {
        display: grid;
        grid-template-columns: 1fr 2fr;
        border-radius: 10px;
        padding: 0.5rem;
        background-color: white;
    }

    h1, h2 {
        font-size: 1rem;
        text-overflow: ellipsis;
        font-family: Arial, Helvetica, sans-serif;
    }

    h2 {
        text-align: start;
    }

    .long {
        text-overflow: ellipsis;
        white-space: nowrap;
        overflow: hidden;
    }

    .delete-btn {
        top: 1rem;
        left: 1rem;
    }
<!DOCTYPE html>
<html lang="en">

<head>
    <link rel="stylesheet" media="screen" href="https://fontlibrary.org//face/comili" type="text/css" />
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <div class="nav">My Home Library</div>
    <div class="main">
        <div class="content">
            <div class="form-container">
                <button id="new-book" class="button-1">New Book</button>
                <form action="#" method="post" class='form-hidden'>
                    <div>
                        <label for="author">Author:</label>
                        <input type="text" name="Author" id="author">
                    </div>
                    <div>
                        <label for="title">Title:</label>
                        <input type="text" name="Title" id="title">
                    </div>
                    <div>
                        <label for="pages">Number of pages:</label>
                        <input type="number" name="Pages" id="pages">
                    </div>
                    <div>
                        <label for="status">Have I read the book?: </label>
                        <input type="text" name="Status" id="status">
                    </div>
                    <button class="button-2" type="button" id="form-button">Add a new book</button>
                </form>
            </div>
        </div>
    </div>
    <div class="footer">footer</div>
</body>
</html>
trincot
  • 317,000
  • 35
  • 244
  • 286
  • This propagates another issue in that it produces invalid HTML as the OP had (same issue) `btn.setAttribute('id', 'delete-btn');` just FYI – Mark Schultheiss Jul 05 '22 at 22:02
  • @MarkSchultheiss, didn't I mention this in my answer? – trincot Jul 06 '22 at 10:49
  • Yes, to be fair you did - note your solution actually still works even if you comment out that line that adds the ID here which then leaves all 4 delete buttons on each book piled on top of each other at the same location - it appears only the first then fire the event when clicked and the book gets deleted. – Mark Schultheiss Jul 06 '22 at 12:07
0

I was able to reproduce by adding two books, then deleting the first.

The problem is that you're adding an event listener again when the second form is submitted.

You need to assign a unique id to each delete button so that the listener is only added once, or better, don't use an ID at all.

The reason it works is:

  1. The first time the listener runs, the book is successfully removed.
  2. However, the second (or third, fourth) time the listener runs, it errors. This is becuase all of your Delete buttons have the same HTML ID and you use a foreach to add listeners.
  3. The reason you need at least two books to reproduce this issue is that the foreach will add the listener again to the first book, causing the bug (the second book will NOT have the bug).

Solution:

Move your event listener to the createBooks function, only adding it once.

            btn.setAttribute('data-key', `${bookObj}`);

+           btn.addEventListener('click', e => {
+               const btnDataKey = parseInt(btn.getAttribute('data-key'));
+               console.log(document.getElementById(btnDataKey));
+               content.removeChild(document.getElementById(btnDataKey));
+           })

            // const number = btn.getAttribute('data-key');
            // console.log(number);

tresf
  • 7,103
  • 6
  • 40
  • 101