3
\$\begingroup\$

I'm working my way through the Foundations course on The Odin Project and just finished the Etch-a-Sketch assignment.

The program receives a number from 1 - 100 from the user through a prompt and then creates a grid from the square of the number. Then, on mouse over, one square of the grid will be assigned a random number and will have its opacity increased by 0.10. When the user wishes to create a new grid, they click a button that eliminates the current grid and then reruns in the same way as before.

I am so new at programming and I really hope to learn "best practices" and how to code without the need for too many comments. I also had a hard time selecting a child node of the container and applying style changes to the node on mouse over.

To me, any feed back is invaluable. Thank you for any considerations.

function removeAllChildNodes(parent){
    while (parent.firstChild) {
        parent.removeChild(parent.firstChild);
    }
}

function getInput() {
    let promptInput = prompt('Enter a whole number between 1 and 100', '');
    return promptInput;
}

function createArray(promptInput){
    const square = [];

    for(i = 0; i < promptInput; i++) {
        square[i] += `${i}`;
    }
    return square;
}

let uniqueClass = 0;
function createUniqueClass(){
    
    uniqueClass += 1;
    return uniqueClass;
    
}

function addOpacity(element) {
    let numElement = +(element.style.opacity);
    if (numElement === 1){
        return numElement;
    }
    numElement += 0.10;
    return numElement;
}

function addColor(div){
    div.addEventListener('mouseover', (event) => {
        const element = document.getElementsByClassName(event.target.classList);
        console.log(event.target);
        element[0].style.background = `#${(Math.floor(Math.random()*16777215).toString(16))}`
        element[0].style.opacity = `${addOpacity(element[0])}`;
    });
}

const container = document.querySelector('#container');
container.setAttribute('style', 'display: flex; flex: 1 1 0; height: 960px; width: 960px; margin: auto; border: solid black 1px; flex-wrap: wrap;');

function createGrid(grid){

    grid.forEach(() => {
        const div = document.createElement('div');
        div.setAttribute('style', 'box-sizing: border-box; border: 1px solid black;');
        /*div.style.boxSizing = 'border-box'; // div.setAttribute would not accept boxSizing property
        div.style.border = '1px solid black';*/
        div.classList.add(`box${createUniqueClass()}`); // Create a unique class name for each div child of container
        container.appendChild(div);
        div.style.height = `${960 / promptInput + 0.01}px`; // Add 0.01px for column doesn't fill in container completely
        div.style.width = `${960 / promptInput}px`;    
        addColor(div);
    });
}

let promptInput = '';
let array = [''];
const button = document.querySelector('button');
    
button.addEventListener('click', () => {
    removeAllChildNodes(container);
    promptInput = getInput();
    grid = createArray(promptInput * promptInput);
    createGrid(grid);
    });
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <script src="java-script.js" defer></script>
  <link rel="stylesheet" href="style.css">
</head>

<body>
  <button>Generate New Grid</button>
  <div id="container"></div>
</body>

</html>

\$\endgroup\$
2
  • 1
    \$\begingroup\$ Minor: use const everywhere out of habit, unless you'll ever change the variable, in which case use let. E.g. const promptInput, for(let i = etc. You seem to have a global uniqueClass. Seems like you're tracking how many instances of something are being created. Why is that necessary? \$\endgroup\$ Commented Sep 4, 2022 at 23:30
  • \$\begingroup\$ Thanks for the feedback! I wrote the global variable above the function because I wanted each div child node of container to have a unique class, i.e <div class=box1>, <div class=box2> etc. I thought that if I had declared the variable in the scope of the createUniqueClass() function it would revert to the original initialized value each time the function was called. This is assuming I wrote let uniqueClass=0 within the function body. In essence I thought it would create a child node of <div class=box1> every time. This was my attempt of achieving this outcome. Maybe there is a better way. \$\endgroup\$ Commented Sep 5, 2022 at 4:15

1 Answer 1

2
\$\begingroup\$

Use prettier and eslint

Code auto-formatters are awesome. Try one at https://prettier.io/playground and look into adding it to your build which will make your code easier for other developers to read.

Linters automatically tell you about many problems in your code. A good portion of code reviews on this site are pretty much repeating linter output. Try eslint at https://eslint.org/play/.

Use const rather than let

In general, never use let unless you really need to mutate something, like a loop counter. Otherwise, use const. const guarantees the variable will never be reassigned, making the code easier to read (one less possible state change to reason about) and avoids a whole class of potential bugs. A codebase chock-full of lets is a sure sign of other problems.

Avoid pointless intermediate variables

Minor point, but

function getInput() {
    let promptInput = prompt('Enter a whole number between 1 and 100', '');
    return promptInput;
}

could be

const getInput() => prompt('Enter a whole number between 1 and 100');

Even so, prompt is generally considered to provide a poor UX, so I'd use an <input type="number" min="1" max="100"> element.

I prefer arrow functions but the classic function syntax with return is acceptable too. What I like about the const/arrow function version:

  • Guarantees no reassignments
  • Guarantees the function is only visible below its definition like a normal variable; no magic hoisting
  • Makes it obvious that when you do use function, it's because you want to avoid automatic this binding
  • No need to use return much of the time, encouraging short functions
  • Less typing; cleaner-looking, modern functional style that is more aesthetically pleasing to me.

I don't think you need to specify the second default value argument to prompt if it's just an empty string.

Perform input conversions

prompt() always returns a string, and JS is "smart" enough to parse strings to integers when using the * operator, but it's best to make that conversion by hand and create a number as soon as possible after receving the input. This separates presentation and logic, making refactoring and debugging easier and helps mitigate surprising type bugs, which JS is especially prone to. Implicit conversion is generally seen as a flaw in JS' design rather than a feature. Hence TypeScript.

Validate input

UX: if the user enters invalid input, nothing happens. I suggest re-prompting the user when they enter something invalid.

It's also not entirely clear to what the number the user is entering represents exactly, based on the prompt. Maybe explain that it's the grid side length.

Remove debugging logs and commented-out code

console.log(event.target); spams the console when I run your snippet. These logs slow down the app and are unsightly.

Similarly, commented-out code shouldn't be committed or pushed to production.

Use high-level JS array functions

Avoid counter-based for loops in code. There's almost always more idiomatic way.

function createArray(promptInput){
    const square = [];

    for(i = 0; i < promptInput; i++) {
        square[i] += `${i}`;
    }
    return square;
}

could be

const createArray = n => [...Array(n)].map((_, i) => `${i}`);

There's a deeper reason for doing this other than aesthetics--your loop counter i creates a global variable attached to the window which could easily interfere with code elsewhere. Always "use strict"; at the top of your scripts to help avoid such errors. With traditional for loops, it's all too easy to mess up the index or .length and introduce bugs. A classic bug is typoing .lenght.

Also, traditional for loops don't self-document the code--it's hard to tell whether they're acting as filters, mappers, reducers, searchers, existence checks or initializers at a glance.

Notice that the function name, createArray, sounds pretty generic, but it then has a confusing promptInput parameter that doesn't make much sense taken out of context and implicitly couples it to the code around it. I'd just call this parameter n. Also, it doesn't just create any old array, it creates an array of string numbers from 0..n. Perhaps createAscendingNumberArray is a more precise name.

Don't generate dynamic classes or ids

div.classList.add(`box${createUniqueClass()}`) and the createUniqueClass are antipatterns. Rather than using dynamic classes, just add a single .box class and select the n-th item of elements matching that class if you need to. But when you're creating dynamic elements, you can just track them in an in-memory data structure so you don't have to select them. This helps reduce two-way data flow in the program.

Event delegation is a useful way to figure out which element in a grid of children had the event triggered on it.

Separate CSS and JS

Your code leaks too much CSS into the JS logic.

For example,

container.setAttribute('style', 'display: flex; flex: 1 1 0; height: 960px; width: 960px; margin: auto; border: solid black 1px; flex-wrap: wrap;');

Imagine needing to adjust this style. It'd be easy to find and modify in a CSS file organized by its class, but baked into JS as a string risks burying it in the codebase. Try to limit your JS to adding, removing and toggling CSS classes and minimize touching element.style.

Separation of structure (HTML), presentation (CSS) and behavior (JS) makes front-end codebases easier to maintain. For larger apps, use a component-based system as most SPA frameworks and libraries do these days. If you do use CSS-in-JS, use a template literal string and keep it to one CSS property per line.

Watch out for floating-point weirdness

The code below stores a two-way bound property in the DOM, then re-parses it back into memory per call:

function addOpacity(element) {
    let numElement = +(element.style.opacity);
    if (numElement === 1){
        return numElement;
    }
    numElement += 0.10;
    return numElement;
}

Concerning is numElement === 1 after incrementing += 0.1 per click. It's probably fine in JS, but in other languages, comparing 1.0 and 1.0 is often not going to work. Safer to use >= 1 or an epsilon comparison.

addOpacity also in moves half of its logic to the caller:

element[0].style.opacity = `${addOpacity(element[0])}`;

Why not complete the abstraction and do that assignment in the addOpacity function? Also, the template literal is unnecessary--the string conversion is already done under the hood.

Similarly, addColor has a confusing contract. It really adds an event listener that adds color only later when and if the user mouses over rather than immediately as the name suggests. I'd call it changeColorOnMouseover or something like that.

Don't query the DOM unnecessarily

Also, the addColor mouseover listener re-queries all elements with a class name when there's supposedly only one. document.querySelector would probably be easier: return the one and only, skipping the [0]. Or drop the query entirely and use event.target which is the hovered element.

Even better, just use the element you're adding the listener to!

Create the right abstractions

Your criteria for when to break out logic seems arbitrary. For example, why was getInput broken out into its own function, but not the code to produce a random color, #${(Math.floor(Math.random()*16777215).toString(16))}? The former is a one-liner with no general value that's only called from a single place, whereas the latter is ugly to look at in-line and could easily be a general-purpose library function in an npm package.

Be careful of creating functions without good reason to. When you do, it should be to clean up the logic in the caller by breaking out sub-steps, to enable code reuse and maintainability and to generalize repeated code.

Suggested rewrite

I left prompt in and kept reading opacity from the DOM, but if the app were any larger I'd consider creating objects for each cell to keep track of opacity and other properties in memory, then reflect these properties to the DOM in one direction as described above.

"use strict";

const randomHexColor = () =>
  `#${Math.floor(Math.random() * 16777215).toString(16)}`;

const removeAllChildNodes = parent => {
  while (parent.firstChild) {
    parent.removeChild(parent.firstChild);
  }
};

const increaseOpacity = (el, by = 0.1) => {
  el.style.opacity = Math.min(1, +el.style.opacity + by);
};

const makeGridCell = size => {
  const div = document.createElement("div");
  div.style.width = div.style.height = `${size}px`;
  div.classList.add("grid-cell");
  return div;
};

const addGridMouseoverListener = () => {
  const grid = document.querySelector(".grid-container");
  grid.addEventListener("mouseover", event => {
    const cell = event.target.closest("div");

    if (
      cell &&
      cell.classList.contains("grid-cell") &&
      grid.contains(cell)
    ) {
      increaseOpacity(cell);
      cell.style.background = randomHexColor();
    }
  });
};

const addNewGridButtonListener = () => {
  const grid = document.querySelector(".grid-container");
  const button = document.querySelector(".new-grid");
  button.addEventListener("click", () => {
    removeAllChildNodes(grid);
    const message = "Enter a whole number between 1 and 100";
    const sideLen = +prompt(message);
    const w = parseInt(getComputedStyle(grid).width);

    for (let i = 0; i < sideLen * sideLen; i++) {
      grid.appendChild(makeGridCell(w / sideLen));
    }
  });
};

addGridMouseoverListener();
addNewGridButtonListener();
.grid-container {
  display: flex;
  flex: 1 1 0;
  flex-wrap: wrap;
  height: 960px;
  width: 960px;
  margin: auto;
  border: solid black 1px;
}

.grid-cell {
  box-sizing: border-box;
  border: 1px solid black;
}
<button class="new-grid">Generate New Grid</button>
<div class="grid-container"></div>

\$\endgroup\$
1
  • \$\begingroup\$ Thank you so much for your response! I wish I could respond to it all. I'm going to save this as a reference for future projects. Your point about abstraction really helps me. I was making functions without a good reason, so I'll keep in mind what you said. I'm especially interested in event delegation as I really struggled with that on this project. Thank you for linking to more information on that. I'm going to study your rewrite until I really understand it in the context of your response. I definitely have a lot to learn, thank you for helping me take a step in the right direction. \$\endgroup\$ Commented Sep 6, 2022 at 22:42

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.