Sort Descending for Javascript cards - javascript

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()
}
}

There are a few ways you can do this:
Pass a reference to a DOM node where you want to append your Drink cards
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:
First, as you pointed out, an element with id rendered-content does not exist in your DOM
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:
Add <div id="rendered-content"></div> in your HTML somewhere the sorted cards needs to be rendered
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:
Your HTML document is retrieved by the browser and parsed
A DOM tree is then created from the parsed document
Layout is applied to the DOM (CSS)
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.

Related

how to stop repetition of previous post

In the displayLikedPosts there is a function called getLikedPosts. this getLikedPosts function filter the posts ID and insert posts into an array. Now after running forEach loop inside the displayLikedPosts ---first time it adds a new and single post(ex:post-1) but second time it displays the previous post twice and the new post (ex: post-1,post-1,newpost). But my purpose is to display only the previous one and the new posts(ex:post-1,newpost,newpost2,newpost4)
code
const displayLikedPosts = () => {
const likedPosts = getLikedPosts();
likedPosts.forEach((post) => {
const div = createPost(post);
document.getElementById("liked").appendChild(div);
});
};
Clear thr content inside #liked before appending new elements
Pseudo Code
const displayLikedPosts = () => {
const likedPosts = getLikedPosts();
document.getElementById("liked")innerHTML = "";
likedPosts.forEach((post) => {
const div = createPost(post);
document.getElementById("liked").appendChild(div);
});
};
You must first delete the children of your parent DOM node, then add the
elements again.
There are many ways to delete the children, which are discussed in this thread. You may find that some options are better (and/ or more modern) than others.
Here is a small minimal example for your use case using setInterval() to prove that the list does not contain duplicates.
/**
* Display posts
* #param {string[]} posts posts to display
*/
function displayPosts(posts) {
const parent = document.getElementById("liked");
// delete children
deleteChildren(parent);
// add items to the list
posts.forEach((post) => {
const listItem = document.createElement("li");
listItem.textContent = post;
parent.appendChild(listItem);
});
console.log("rendered")
}
/**
* Deletes all children of a DOM node.
* #param {HTMLElement} parent parent element
*/
function deleteChildren(parent) {
while (parent.firstChild) {
parent.firstChild.remove();
}
}
window.addEventListener("DOMContentLoaded", (event) => {
// posts to display
const posts = ["post 1", "post 2", "post 3", "post 4"];
displayPosts(posts)
// call method every 3 seconds to demonstrate it works
setInterval(() => displayPosts(posts), 3 * 1000);
});
<ul id="liked">
</ul>

How can I group Javascript actions that force reflow?

I have a project which is responsible for managing the rendering of elements, but I'm running into a performance issue replacing elements and then focusing on whatever had focus before.
Below is a minimal example that replicates the performance issue:
const renderPage = () => {
// get the old section element
const oldSection = document.querySelector('section')
// create a new section element (we'll replaceWith later)
const newSection = document.createElement('section')
// create the render button
const newButton = document.createElement('button')
newButton.innerHTML = 'Render Page'
newButton.onclick = renderPage
newSection.appendChild(newButton)
// create a bunch of elements
const dummyDivs = [...new Array(100000)].forEach(() => {
const dummy = document.createElement('div')
dummy.innerHTML = 'dummy'
newSection.appendChild(dummy)
})
// replace the old page with the new one (causes forced reflow)
oldSection.replaceWith(newSection)
// reattach focus on the button (causes forced reflow)
newButton.focus()
}
window.renderPage = renderPage
<section>
<button onclick="renderPage()">Render</button>
</section>
When running this locally, I see the following in the performance report in Chrome/Edge
Both replaceWith and focus are triggering forced reflow. Is there a way to batch or group these actions so that only a single reflow occurs? I realize that there's no way to really get around this happening at all, but if I can batch them, I think that might improve my performance.
Indeed, focus always causes a reflow: What forces layout / reflow
So what you may do, is to reduce the reflowtime by inserting the new button standalone, initiate focus and after that you can append other childs:
Working example: Example
const renderPage = () => {
// get the old section element
const oldSection = document.querySelector('section')
// create a new section element (we'll replaceWith later)
const newSection = document.createElement('section')
// create the render button
const newButton = document.createElement('button')
newButton.innerHTML = 'Render Page'
newButton.onclick = renderPage
newSection.appendChild(newButton)
// create a bunch of elements
const dummies = []; // store in seperate array
const dummyDivs = [...new Array(100000)].forEach(() => {
const dummy = document.createElement('div')
dummy.innerHTML = 'dummy';
dummies.push(dummy)
})
// insert new section only with new button
oldSection.replaceWith(newSection)
newButton.focus(); // always causes reflow; but fast because it's only one element
// store all other nodes after focus
newSection.append(...dummies)
}
window.renderPage = renderPage

How to click an element that we get from http request using SetInterval?

window.addEventListener("load" , () => {
setInterval(() => {
let xhttp = new XMLHttpRequest();
xhttp.open("GET", "./messagedUsers.php", true);
xhttp.onload = () => {
if(xhttp.readyState === XMLHttpRequest.DONE) {
if(xhttp.status === 200) {
let data = xhttp.response;
sideActiveUsers.innerHTML = data;
}
}
}
xhttp.send();
} , 500);
messageInputContainer.focus();
})
let userInfo = document.querySelectorAll(".user-info");
userInfoClick(userInfo); // function that does operation using the nodeList userInfo
In the above code , what I'm trying to do is send a HttpRequest to the php and get a div container as a response from there. As a response , I do get the div container. The div I got from there has a class name "user-info". And I want to click it and further , apply some css to the all the elements it holds(as there will be lot of "user-info" container coming from the response.). What I'm unable to figure out is "user-info" container can be applied styling through css(not JS). But if I try to use some JS for the node with className "user-info" , nothing happens as the nodeList seems to be empty.
And I've also tried using querySelector inside the setInterval for "user-info". But that makes the function outside userInfoClick(userInfo) takes a userInfo that is not defined.
And if I use both querySelector and the function that takes the variable that holds that nodeList(userInfoClick), I am able to click the element coming back from php. But its effect is stopped after every 500 ms(as the setInterval is set for 500ms).
I want to be able to click the element coming back from php and click it whose effect doesn't change after every 500ms(along with the element getting from php every 500ms).
Help me out guys.
userInfoClick function:
const userInfoClick = (userInfo) => {
let userinfo = Array.from(userInfo);
userinfo.forEach((el , ind) => {
el.addEventListener("click" , () => {
for(let i=0 ; i<userInfo.length ; i++)
{
if(ind!==i)
{
userinfo[i].style.backgroundColor = "#dddddd";
}
}
el.style.backgroundColor = "#fff";
})
})
}
You cannot bind an event handler to an element that does not yet exist. The querySelectorAll call will return an empty collection, as the assignment to innerHTML will only happen in some future.
In this case, where you have the interval, it is better to use event delegation. This means that you listen to click events at a higher level in the DOM tree -- on an element that is always there and is not dynamically created. In your case this could be sideActiveUsers.
So change your code as follows:
Remove this line (as it will return a useless empty collection):
let userInfo = document.querySelectorAll(".user-info");
And change the userInfoClick function, which should be called only once, without any arguments:
function userInfoClick() {
sideActiveUsers.addEventListener("click" , (e) => {
const el = e.target;
// We're only interested in user-info clicks:
if (!el.classList.contains("user-info")) return;
// Since we clicked on a user-info, the following will now have matches:
for (const sibling of sideActiveUsers.querySelectorAll(".user-info")) {
sibling.style.backgroundColor = "#dddddd";
}
el.style.backgroundColor = "#fff";
});
}

how should i dynamically update a firebase document

i'm working on a simple note-taking app for my portfolio using JS and Firebase. Before i tell you what's happening i feel like i need to show you how my code works, if you have any tips and concerns please tell me as it would be GREATLY appreciated. That being said, let's have a look "together". I'm using this class to create the notes:
const htmlElements = [document.querySelector('.notes'), document.querySelector('.note')];
const [notesDiv, noteDiv] = htmlElements;
class CreateNote {
constructor(title, body) {
this.title = title;
this.body = body;
this.render = () => {
const div1 = document.createElement('div');
div1.className = 'notes-prev-container';
div1.addEventListener('click', () => { this.clickHandler(this) });
const div2 = document.createElement('div');
div2.className = 'notes-prev';
const hr = document.createElement('hr');
hr.className = 'notes__line';
// Nest 'div2' inside 'div1'
div1.appendChild(div2);
div1.appendChild(hr);
/*
Create Paragraph 1 & 2 and give them the same
class name and some text
*/
const p1 = document.createElement('p');
p1.className = 'notes-prev__title';
p1.innerText = this.title;
const p2 = document.createElement('p');
p2.className = 'notes-prev__body';
p2.innerText = this.body;
// Nest p 1 & 2 inside 'div2'
div2.appendChild(p1);
div2.appendChild(p2);
// Finally, render the div to its root tag
notesDiv.appendChild(div1);
}
}
/*
Every time this method is called, it creates 2 textareas,
one for the note title and the other for its body then it
appends it to the DOM.
*/
renderNoteContent () {
const title = document.createElement('textarea');
title.placeholder = 'Title';
title.value = this.title;
title.className = 'note__title';
const body = document.createElement('textarea');
body.placeholder = 'Body';
body.value = this.body;
body.className = 'note__body';
noteDiv.appendChild(title);
noteDiv.appendChild(body);
}
/*
When this method is called, it checks to see if there's a
note rendered already (childElementCount === 1 because there's a
button, so if there's only this button it means there's no
textareas rendered).
If yes, then merely call the renderNoteContent method. Else
get the tags with the classes 'note__title' and 'note__body'
and remove them from the DOM, then call renderNoteContent to
create the textareas with the clicked notes values.
This function gets mentioned at line 19.
*/
clickHandler(thisClass) {
if (noteDiv.childElementCount === 1) {
thisClass.renderNoteContent();
} else {
document.querySelector('.note__title').remove();
document.querySelector('.note__body').remove();
thisClass.renderNoteContent();
}
}
}
Now i need 2 buttons, createNotesButton and saveNotesButton respectively. These 2 buttons must be inside a function that will be called inside .onAuthStateChanged (why? because they will be needing access to the currentUser on firebase auth).
I want the createNotesButton to create a note prototype, render it to the DOM and create a new document on firestore, where this note contents will be stored. Here's how i did it:
PS: I feel like i'm not using this class correctly, so again if you have any tips i appreciate it.
import {db} from '../../firebase_variables/firebase-variables.js';
import {CreateNote} from '../create_notes_class/create_notes_class.js';
const htmlElements = [
document.querySelector('.createNotes-button'),
document.querySelector('.saveNotes-button')
];
const [createNotesButton, saveNotesButton] = htmlElements;
function clickHandler(user) {
/*
1. Creates a class.
2. Creates a new document on firebase with the class's empty value.
3. Renders the empty class to the DOM.
*/
createNotesButton.addEventListener('click', () => {
const note = new CreateNote('', '');
note.render();
// Each user has it's own note collection, said collection has their `uid` as name.
db.collection(`${user.uid}`).doc().set({
title: `${note.title}`,
body: `${note.body}`
})
})
}
Now i need a saveNotesButton, he's the one i'm having issues with. He needs to save the displayed note's content on firestore. Here's what i tried doing:
import {db} from '../../firebase_variables/firebase-variables.js';
import {CreateNote} from '../create_notes_class/create_notes_class.js';
const htmlElements = [
document.querySelector('.createNotes-button'),
document.querySelector('.saveNotes-button')
];
const [createNotesButton, saveNotesButton] = htmlElements;
function clickHandler(user) {
createNotesButton.addEventListener('click', () => {...})
/*
1. Creates 2 variables, `title` and `body, if there's not a note being displayed
their values will be null, which is why the rest of the code is inside an if
statement
2. If statement to check if there's a note being displayed, if yes then:
1. Call the user's note collection. Any document who has the title field equal to the
displayed note's value gets returned as a promise.
2. Then call an specific user document and update the fields `title` and `body` with
the displayed note's values.
3. If no then do nothing.
*/
saveNotesButton.addEventListener('click', () => {
const title = document.querySelector('.note__title');
const body = document.querySelector('.note__body');
db.collection(`${user.uid}`).where('title', '==', `${title.value}`)
.get()
.then(userCollection => {
db.collection(`${user.uid}`).doc(`${userCollection.docs[0].id}`).update({
title: `${title.value}`,
body: `${body.value}`
})
})
.catch(error => {
console.log('Error getting documents: ', error);
});
});
}
This didn't work because i'm using title.value as a query, so if i change it's value it will also change the queries direction to a path that doesn't exist.
So here's the question: how can i make it so the saveNotesButton does its job? I was thinking of adding another field to each note, something that won't change so i can easily identify and edit each note. Again, if there's something in my code that you think can or should be formatted please let me know, i'm using this project as a way to solidify my native JS knowledge so please be patient. I feel like if i had used React i would've finished this sometime ago but definitely wouldn't have learned as much, anyway thanks for your help in advance.
I was thinking of adding another field to each note, something that won't change so i can easily identify and edit each note.
Yes, you absolutely need an immutable identifier for each note document in the firestore so you can unambiguously reference it. You almost always want this whenever you're storing a data object, in any application with any database.
But, the firestore already does this for you: after calling db.collection(user.uid).doc() you should get a doc with an ID. That's the ID you want to use when updating the note.
The part of your code that interacts with the DOM will need to keep track of this. I suggest moving the code the creates the firestore document into the constructor of CreateNote and storing it on this. You'll need the user id there as well.
constructor(title, body, userId) {
this.title = title;
this.body = body;
const docRef = db.collection(userId).doc();
this.docId = docRef.id;
/* etc. */
Then any time you have an instance of CreateNote, you'll know the right user and document to reference.
Other suggestions (since you asked)
Use JsPrettier. It's worth the setup, you'll never go back.
Use HTML semantics correctly. Divs shouldn't be appended as children of hrs, because they're for "a thematic break between paragraph-level elements: for example, a change of scene in a story, or a shift of topic within a section." MDN
For your next project, use a framework. Essentially no one hand-codes event listeners and appends children to get things done. I see the value for basic understanding, but there's a rich and beautiful world of frameworks out there; don't limit yourself by avoiding them :-)

How to remove mentions from an editorstate in draft js?

Been at this one for a LONG time and can't quite figure it out.
I have two components, controlled by a parent component. There is a property called "selected". So, when a user clicks on a list, it will update the parent component's selected property which is passed to the TagInput, which uses a MentionPlugin from draft-js.
In order to handle this, I implement a componentWillReceiveProps that looks as follows.
componentWillReceiveProps(nextProps) {
const { initialTags: newTags } = nextProps;
const previousTags = this.getTags(this.state.editorState);
if (previousTags.length !== newTags.length) {
const added = newTags.filter(tag => !previousTags.includes(tag));
const removed = previousTags.filter(tag => !newTags.includes(tag));
this.addMentions(added);
this.removeMentions(removed);
}
}
While it's easy to add entities in addMentions by creating a new entity and inserting it, for the life of me, I cannot figure out how to get a Mention by text and then delete it from the editor.
removeMentions(tags) {
const { editorState } = this.state;
for (const tag of tags) {
// find tag in editor
// select it and remove it
}
}
How would this be done?

Categories

Resources