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
Related
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.
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 :-)
I'm working on a custom draft.js plugin that inserts an Atomic Block with a GIF in it. I started by copying the Draft.js Image Plugin as it's almost identical. I've got my plugin working but there's one issue I'm not sure best how to solve.
To insert a GIF I'm following the example addImage Modifier in the Image Plugin. However, this always creates a new Atomic Block after the current selection. If the current block is empty, I want to place the GIF there instead.
Here's what my modifier function looks like:
const addGiphy = (editorState, giphyId) => {
const contentState = editorState.getCurrentContent();
// This creates the new Giphy entity
const contentStateWithEntity = contentState.createEntity("GIPHY", "IMMUTABLE", {
giphyId
});
const entityKey = contentStateWithEntity.getLastCreatedEntityKey();
// Work out where the selection is
const currentSelection = editorState.getSelection();
const currentKey = currentSelection.getStartKey();
const currentBlock = editorState.getCurrentContent().getBlockForKey(currentKey);
let newEditorState;
if (currentBlock.getType() === "unstyled" && currentBlock.getText() === "") {
// Current line is empty, we should convert to a gif atomic block
// <INSERT MAGIC HERE>
} else {
// There's stuff here, lets create a new block
newEditorState = AtomicBlockUtils.insertAtomicBlock(editorState, entityKey, " ");
}
return EditorState.forceSelection(
newEditorState,
newEditorState.getCurrentContent().getSelectionAfter()
);
};
I'm not sure how to handle the condition of converting the current block to an Atomic Block. Is this Draft.js best practice? Alternatively, am I better to always insert a new block and then remove the empty block?
For clarity, this same issue also exists in the Image Plugin, it's not something I've introduced.
you can use https://draftjs.org/docs/api-reference-modifier/#setblocktype
const content = Modifier.setBlockType(content, editorState.getSelection(), 'atomic');
const newState = EditorState.push(editorState, content, 'change-block-type');
I've been learning some functional programming with JavaScript lately, and wanted to put my knowledge to the test by writing a simple ToDo app with just functional programming. However, I'm not sure how does one store the state of the list in a pure functional way, since functions are not allowed to have side effects. Let me explain with an example.
Let's say I have a constructor called "Item", which just has the task to be done, and a uuid to identify that item. I also have an items array, which holds all the current items, and an "add" and "delete" functions, like so:
function Item(name){
this.name = name;
this.uuid = uuid(); //uuid is a function that returns a new uuid
}
const items = [];
function addItem(name){
const newItem = new Item(name);
items.push(newItem);
}
function deleteItem(uuid){
const filteredItems = items.filter(item => item.uuid !== uuid);
items = filteredItems
}
Now this works perfectly, but as you can see functions are not pure: they do have side effects and don't return anything. With this in mind, I try to make it functional like this:
function Item(name){
this.name = name;
this.uuid = uuid(); //uuid is a function that returns a new uuid
}
const items = [];
function addItem(array, constructor, name){
const newItem = new constructor(name);
return array.concat(newItem);
}
function removeItem(array, uuid){
return array.filter(item => item.uuid !== uuid);
}
Now the functions are pure (or so I think, correct me if I'm wrong), but in order to store the list of items, I need to create a new array each time I add or remove an item. Not only this seems incredibly inefficient, but I'm also not sure how to properly implement it. Let's say that I want to add a new item to the list each time a button is pressed in the DOM:
const button = document.querySelector("#button") //button selector
button.addEventListener("click", buttonClicked)
function buttonClicked(){
const name = document.querySelector("#name").value
const newListOfItems = addItem(items, Item, name);
}
This is once again not purely functional, but there is yet another problem: this will not work properly, because each time the function gets called, it will create a new array using the existing "items" array, which is itself not changing (always an empty array). To fix this, I can only think of two solutions: modifying the original "items" array or store a reference to the current items array, both of which involve the functions having some kind of side effects.
I've tried to look for ways to implement this but haven't been successful. Is there any way to fix this using pure functions?
Thanks in advance.
The model–view–controller pattern is used to solve the state problem you described. Instead of writing a lengthy article about MVC, I'll teach by demonstration. Let's say that we're creating a simple task list. Here are the features that we want:
The user should be able to add new tasks to the list.
The user should be able to delete tasks from the list.
So, let's dive in. We'll begin by creating the model. Our model will be a Moore machine:
// The arguments of createModel are the state of the Moore machine.
// |
// v
const createModel = tasks => ({
// addTask and deleteTask are the transition functions of the Moore machine.
// They return new updated Moore machines and are purely functional.
addTask(task) {
if (tasks.includes(task)) return this;
const newTasks = tasks.concat([task]);
return createModel(newTasks);
},
deleteTask(someTask) {
const newTasks = tasks.filter(task => task !== someTask);
return createModel(newTasks);
},
// Getter functions are the outputs of the Moore machine.
// Unlike the above transition functions they can return anything.
get tasks() {
return tasks;
}
});
const initialModel = createModel([]); // initially the task list is empty
Next, we'll create the view which is a function that given the output of the model returns a DOM list:
// createview is a pure function which takes the model as input.
// It should only use the outputs of the model and not the transition functions.
// You can use libraries such as virtual-dom to make this more efficient.
const createView = ({ tasks }) => {
const input = document.createElement("input");
input.setAttribute("type", "text");
input.setAttribute("id", "newTask");
const button = document.createElement("input");
button.setAttribute("type", "button");
button.setAttribute("value", "Add Task");
button.setAttribute("id", "addTask");
const list = document.createElement("ul");
for (const task of tasks) {
const item = document.createElement("li");
const span = document.createElement("span");
span.textContent = task;
const remove = document.createElement("input");
remove.setAttribute("type", "button");
remove.setAttribute("value", "Delete Task");
remove.setAttribute("class", "remove");
remove.setAttribute("data-task", task);
item.appendChild(span);
item.appendChild(remove);
list.appendChild(item);
}
return [input, button, list];
};
Finally, we create the controller which connects the model and the view:
const controller = model => {
const app = document.getElementById("app"); // the place we'll display our app
while (app.firstChild) app.removeChild(app.firstChild); // remove all children
for (const element of createView(model)) app.appendChild(element);
const newTask = app.querySelector("#newTask");
const addTask = app.querySelector("#addTask");
const buttons = app.querySelectorAll(".remove");
addTask.addEventListener("click", () => {
const task = newTask.value;
if (task === "") return;
const newModel = model.addTask(task);
controller(newModel);
});
for (const button of buttons) {
button.addEventListener("click", () => {
const task = button.getAttribute("data-task");
const newModel = model.deleteTask(task);
controller(newModel);
});
}
};
controller(initialModel); // start the app
Putting it all together:
// The arguments of createModel are the state of the Moore machine.
// |
// v
const createModel = tasks => ({
// addTask and deleteTask are the transition functions of the Moore machine.
// They return new updated Moore machines and are purely functional.
addTask(task) {
if (tasks.includes(task)) return this;
const newTasks = tasks.concat([task]);
return createModel(newTasks);
},
deleteTask(someTask) {
const newTasks = tasks.filter(task => task !== someTask);
return createModel(newTasks);
},
// Getter functions are the outputs of the Moore machine.
// Unlike the above transition functions they can return anything.
get tasks() {
return tasks;
}
});
const initialModel = createModel([]); // initially the task list is empty
// createview is a pure function which takes the model as input.
// It should only use the outputs of the model and not the transition functions.
// You can use libraries such as virtual-dom to make this more efficient.
const createView = ({ tasks }) => {
const input = document.createElement("input");
input.setAttribute("type", "text");
input.setAttribute("id", "newTask");
const button = document.createElement("input");
button.setAttribute("type", "button");
button.setAttribute("value", "Add Task");
button.setAttribute("id", "addTask");
const list = document.createElement("ul");
for (const task of tasks) {
const item = document.createElement("li");
const span = document.createElement("span");
span.textContent = task;
const remove = document.createElement("input");
remove.setAttribute("type", "button");
remove.setAttribute("value", "Delete Task");
remove.setAttribute("class", "remove");
remove.setAttribute("data-task", task);
item.appendChild(span);
item.appendChild(remove);
list.appendChild(item);
}
return [input, button, list];
};
const controller = model => {
const app = document.getElementById("app"); // the place we'll display our app
while (app.firstChild) app.removeChild(app.firstChild); // remove all children
for (const element of createView(model)) app.appendChild(element);
const newTask = app.querySelector("#newTask");
const addTask = app.querySelector("#addTask");
const buttons = app.querySelectorAll(".remove");
addTask.addEventListener("click", () => {
const task = newTask.value;
if (task === "") return;
const newModel = model.addTask(task);
controller(newModel);
});
for (const button of buttons) {
button.addEventListener("click", () => {
const task = button.getAttribute("data-task");
const newModel = model.deleteTask(task);
controller(newModel);
});
}
};
controller(initialModel); // start the app
<div id="app"></div>
Of course, this is not very efficient because you are updating the entire DOM every time the model is updated. However, you can use libraries like virtual-dom to fix that.
You may also look at React and Redux. However, I'm not a big fan of it because:
They use classes, which makes everything verbose and clunky. Although, you can make functional components if you really want to.
They combine the view and the controller, which is bad design. I like to put models, views, and controllers in separate directories and then combine them all in a third app directory.
Redux, which is used to create the model, is a separate library from React, which is used to create the view–controller. Not a dealbreaker though.
It's unnecessarily complicated.
However, it is well-tested and supported by Facebook. Hence, it's worth looking at.
const add_compressor = (number) => {
let div = document.createElement('div');
div.className = 'compressor';
let threshold = document.createElement('input');
threshold.type = 'number';
threshold.className = 'input_number';
threshold.addEventListener('input', () => {
compressors[number].threshold.value = threshold.value;
})
div.appendChild(threshold);
added_effects.appendChild(div);
}
add_button.addEventListener('click', () => {
if (select_effect.value != 'Add Effect...') {
if (select_effect.value === 'compressor') {
compressors[compressors.length] = effects[select_effect.value];
add_compressor(compressors.length - 1);
}
}
})
I am trying to allow the creation of more than 1 compressor for the Web Audio API with this add_compressor function. Everything works except the threshold.addEventListener is linked to every compressor that is created. I am wondering if it is possible to create the event listeners for multiple compressors this way. Here is a JS fiddle showing the problem in the console.
https://jsfiddle.net/0L1my6kp/
Changing any of the input boxes for the compressors will change the threshold.value for all compressors.
The problem is that you only create a compressor object once, and reference the same object every time.
So instead of this
const effects = {
compressor: context.createDynamicsCompressor()
}
it should be
const effects = {
compressor: context.createDynamicsCompressor
}
then you create a new compressor when you click the button. Check out the updated fiddle: https://jsfiddle.net/0L1my6kp/4/