MarrixRed
MarrixRed

Reputation: 57

Sort Descending for Javascript cards

I am trying to create a sorting method that sorts the cards in the DOM from Z to A when I press a button. So far, I have created the logic to sort the array correctly but can't seem to render it out.

I have a Drink Class and a DrinkCard Class, and the DrinkCard does the actual card creation, and the Drink creates the Drink.

I feel like calling the Drink class would help render sorted array to the DOM, but not sure how I would do that. Drawing blanks.

This is what I have so far UPDATE I updated with the suggestion below, but I don't have a rendered-content id anywhere. So, I used the querySelector on the class .card and this is the current error.

Uncaught DOMException: Failed to execute 'appendChild' on 'Node': The new child element contains the parent.
    at Drink.render (file:///Users/austinredmond/dev/caffeine_me/frontend/src/models/drink.js:28:17)
    at file:///Users/austinredmond/dev/caffeine_me/frontend/src/index.js:43:38
    at Array.forEach (<anonymous>)
    at HTMLInputElement.<anonymous> (file:///Users/austinredmond/dev/caffeine_me/frontend/src/index.js:43:17)
render @ drink.js:28
(anonymous) @ index.js:43
(anonymous) @ index.js:43
sortDesc.addEventListener("click", () => {
   const sortedArray = allDrinks.sort((a, b) => {
        const nameA = a.name.toLowerCase(),
            nameB = b.name.toLowerCase()
        if (nameA < nameB) //sort string ascending
            return 1
        if (nameA > nameB)
            return -1
        return 0 //default return value (no sorting)
    })

    const node = document.querySelector('.card');
    sortedArray.forEach(card => card.render(node));

})

Drink Class

class Drink {

    constructor(data) {
        // Assign Attributes //
        this.id = data.id
        this.name = data.name
        this.caffeine = data.caffeine
        this.comments = []

        this.card = new DrinkCard(this, this.comments)

    }

    // Searches allDrinks Array and finds drink by id //
    static findById(id) {
        return allDrinks.find(drink => drink.id === id)
    }

    // Delete function to Delete from API //
    delete = () => {
        api.deleteDrink(this.id)
        delete this
    }

    render(element) {
        // this method will render each card; el is a reference to a DOM node
        console.log(element)
        element.appendChild(this.card.cardContent);

    }
}

DrinkCard Class

class DrinkCard {
    constructor(drink, comments) {
        // Create Card //
        const card = document.createElement('div')
        card.setAttribute("class", "card w-50")
        main.append(card)
        card.className = 'card'

        // Add Nameplate //
        const drinkTag = document.createElement('h3')
        drinkTag.innerText = drink.name
        card.append(drinkTag)

        // Add CaffeinePlate //
        const caffeineTag = document.createElement('p')
        caffeineTag.innerText = `Caffeine Amount - ${drink.caffeine}`
        card.append(caffeineTag)

        // Adds Create Comment Input Field //
        const commentInput = document.createElement("input");
        commentInput.setAttribute("type", "text");
        commentInput.setAttribute("class", "input-group mb-3")
        commentInput.setAttribute("id", `commentInput-${drink.id}`)
        commentInput.setAttribute("placeholder", "Enter A Comment")
        card.append(commentInput);

        // Adds Create Comment Button //
        const addCommentButton = document.createElement('button')
        addCommentButton.innerText = "Add Comment"
        addCommentButton.setAttribute("class", "btn btn-primary btn-sm")
        card.append(addCommentButton)
        addCommentButton.addEventListener("click", () => this.handleAddComment())

        // Add Comment List //
        this.commentList = document.createElement('ul')
        card.append(this.commentList)

        comments.forEach(comment => this.addCommentLi(comment))

        // Create Delete Drink Button
        const addDeleteButton = document.createElement('button')
        addDeleteButton.setAttribute("class", "btn btn-danger btn-sm")
        addDeleteButton.innerText = 'Delete Drink'
        card.append(addDeleteButton)
        addDeleteButton.addEventListener("click", () => this.handleDeleteDrink(drink, card))

        // Connects to Drink //
        this.drink = drink

        this.cardContent = card;
    }

    // Helpers //

    addCommentLi = comment => {
        // Create Li //
        const li = document.createElement('li')
        this.commentList.append(li)
        li.innerText = `${comment.summary}`

        // Create Delete Button
        const button = document.createElement('button')
        button.setAttribute("class", "btn btn-link btn-sm")
        button.innerText = 'Delete'
        li.append(button)
        button.addEventListener("click", () => this.handleDeleteComment(comment, li))
    }

    // Event Handlers //
    // Handle Adding Comment to the DOM //
    handleAddComment = () => {
        const commentInput = document.getElementById(`commentInput-${this.drink.id}`)
        api.addComment(this.drink.id, commentInput.value)
            .then(comment => {
                commentInput.value = ""
                const newComment = new Comment(comment)
                Drink.findById(newComment.drinkId).comments.push(newComment)
                this.addCommentLi(newComment)
            })
    }

    // Loads last comment created for drink object //
    handleLoadComment = () => {
        const newComment = this.drink.comments[this.drink.comments.length - 1]
        this.addCommentLi(newComment)
    }

    // Deletes Comment from API and Removes li //
    handleDeleteComment = (comment, li) => {
        comment.delete()
        li.remove()
    }

    // Deletes Drink from API and Removes drink card //
    handleDeleteDrink = (drink, card) => {
        drink.delete()
        card.remove()
    }
}

Upvotes: 3

Views: 1636

Answers (1)

Abrar Hossain
Abrar Hossain

Reputation: 2702

There are a few ways you can do this:

  1. Pass a reference to a DOM node where you want to append your Drink cards
  2. Get the raw HTML from the card and add it to an element as needed

For (1), try the following changes:

DrinkCard.js

class DrinkCard {
    constructor(drink, comments) {
         // your existing code
         this.cardContent = card; // or card.innerHTML
    }
}

Drink.js

class Drink {
   // your existing code

   render(el) {
      // this method will render each card; el is a reference to a DOM node
      el.appendChild(this.card.cardContent);

   }
}

Finally, passing the DOM reference to the sorted entries:

const node = document.getElementById('rendered-content'); // make sure this exists

sortedArray.forEach(card => card.render(node));

Hopefully that will give you some pointers on how to render the cards for your purpose.

Updates

The error you are getting is because of the following reasons:

  1. First, as you pointed out, an element with id rendered-content does not exist in your DOM
  2. Using .card to append the rendered element results in a cyclical error because you are trying to append an element (.card) to the same element.

You can try the following:

  1. Add <div id="rendered-content"></div> in your HTML somewhere the sorted cards needs to be rendered
  2. If you don't want to have it in the HTML page all the time, you can create it before you pass it's reference. So,
const rc = document.createElement('div');
rc.setAttribute('id', 'rendered-content');
document.body.appendChild(rc);

const node = document.getElementById('rendered-content');
sortedArray.forEach(card => card.render(node));

This should help get rid of the errors hopefully.

Further Explanation

I am going to give you a very brief description of browser rendering and how it's working in this case. I will also leave a link for a detailed article that goes into more depth.

In the rendering flow, the following happens:

  1. Your HTML document is retrieved by the browser and parsed
  2. A DOM tree is then created from the parsed document
  3. Layout is applied to the DOM (CSS)
  4. Paint the content of the DOM to the display

Your code took care of almost everything in the original post. You created the card, added it's content and sorted the cards based on Drink type. The only step missing was adding it all to the DOM.

If you create elements dynamically like you did in the DrinkCard class, you need to attach it to your existing DOM. Otherwise, there is no way for the browser to know your card is in the DOM. Once you modify the DOM, layout and repainting is triggered which then shows your content on the display.

The purpose of div with id='rendered-content' is to provide a container that exists in your DOM or is added before you use it. When you are adding nodes to your DOM, you need a reference element where you want to add your new node. This reference could easily be document.body. In that case, the render method will add the card at the bottom of your body in your DOM. Providing a separate container in this case gives you more control on how you can display this container.

Here's an in depth discussion of rendering and how it works in the browser here. Hope the explanation answers your question.

Upvotes: 2

Related Questions