3

I have a condition that when the input value is greater than 100, another bottle is added, but I don't know where to insert another functions so that the anothers bottles displays the water level I need, i want set this for eight bottles. I was trying to add a new function to a function addBottle ans others function, but only got errors

let inputWaterLevel = document.querySelector('[name=water-level]');
let heightLiquid = +tubeLiquid.getAttribute('height');
let yLiquid = +tubeLiquid.getAttribute('y');
let liquidPercent = heightLiquid/100;

inputWaterLevel.addEventListener('input', updateWaterLevel);

function updateWaterLevel() {
  let value = +this.value;
  
  let height = liquidPercent * value;
  let y = yLiquid + (heightLiquid-height);
  
  tubeLiquid.setAttribute('height', liquidPercent * value )
  tubeLiquid.setAttribute('y', y )

}

let addBottle2 = () => {
    inputWaterLevel.id = 'inpId';
    let inpId = document.getElementById('inpId')
    inpId.addEventListener("input", () => {
        if (inpId.value > 100) {
            document.getElementById("bot2").style.display = 'block';
        }
        else if (inpId.value <= 100){
            document.getElementById("bot2").style.display = 'none';
        }
    });
}
addBottle2()
 
 .cont{
     display: flex;
 }

 #bot2{
     display: none;
 }
<p><input type="number" min="0" max="200" value="100" name="water-level" /></p>

<div class="cont">
    <div class="bot1">
        <svg viewBox="0 0 300 300" width="150">
            <clipPath id="clip">
                <rect x="118" y="68" width="64" height="228" rx="20" ry="20" />
            </clipPath>
            <image width="300" xlink:href="https://i.ibb.co/HGFQYTj/bottle.png" />
            <rect clip-path="url(#clip)" id="tubeLiquid" x="115" y="58" width="70" height="233" fill="#74ccf4" />
        </svg>
    </div>

    <div id="bot2">
        <svg viewBox="0 0 300 300" width="150">
            <clipPath id="clip2">
                <rect x="118" y="68" width="64" height="228" rx="20" ry="20" />
            </clipPath>
            <image width="300" xlink:href="https://i.ibb.co/HGFQYTj/bottle.png" />
            <rect clip-path="url(#clip2)" id="tubeLiquid2" x="115" y="58" width="70" height="233" fill="#74ccf4" />
        </svg>
    </div>

</div>
mplungjan
  • 169,008
  • 28
  • 173
  • 236
Марта
  • 39
  • 5

5 Answers5

1

What you can do is re-use the code to fill the first bottle, but with parameters telling which bottle to update, and which value to subtract from the total percentage, something like this:

let inputWaterLevel = document.querySelector('[name=water-level]');
let heightLiquid = +tubeLiquid.getAttribute('height');
let yLiquid = +tubeLiquid.getAttribute('y');
let liquidPercent = heightLiquid/100;

//tell this code to update bottle 1 without offset
inputWaterLevel.addEventListener('input', function(){
    updateWaterLevel('tubeLiquid', 0);
});

function updateWaterLevel(tubeLiquidId, offset) {
  let value = +inputWaterLevel.value - offset; //using input variable because "this" will be not be the input anymore now the call has changed / sutract offset
  
  let height = liquidPercent * value;
  let y = yLiquid + (heightLiquid-height);
  
  let tubeLiquid = document.getElementById(tubeLiquidId); //get dynamically liquid to update
  tubeLiquid.setAttribute('height', liquidPercent * value )
  tubeLiquid.setAttribute('y', y )

}

let addBottle2 = () => {
    //those 2 lines are useless, you already have access to "inputWaterLevel" from outer scope, just use it
    //inputWaterLevel.id = 'inpId';
    //let inpId = document.getElementById('inpId')
    inputWaterLevel.addEventListener("input", () => {
        if (inputWaterLevel.value > 100) {
            document.getElementById("bot2").style.display = 'block';
            updateWaterLevel('tubeLiquid2', 100); //the call to update bottle 2 telling the function to remove 100
        }
        else if (inputWaterLevel.value <= 100){
            document.getElementById("bot2").style.display = 'none';
            //no need to update bottle 2 if we're going to hide it
        }
    });
}
addBottle2()
 
 .cont{
     display: flex;
 }

 #bot2{
     display: none;
 }
<p><input type="number" min="0" max="200" value="100" name="water-level" /></p>

<div class="cont">
    <div class="bot1">
        <svg viewBox="0 0 300 300" width="150">
            <clipPath id="clip">
                <rect x="118" y="68" width="64" height="228" rx="20" ry="20" />
            </clipPath>
            <image width="300" xlink:href="https://i.ibb.co/HGFQYTj/bottle.png" />
            <rect clip-path="url(#clip)" id="tubeLiquid" x="115" y="58" width="70" height="233" fill="#74ccf4" />
        </svg>
    </div>

    <div id="bot2">
        <svg viewBox="0 0 300 300" width="150">
            <clipPath id="clip2">
                <rect x="118" y="68" width="64" height="228" rx="20" ry="20" />
            </clipPath>
            <image width="300" xlink:href="https://i.ibb.co/HGFQYTj/bottle.png" />
            <rect clip-path="url(#clip2)" id="tubeLiquid2" x="115" y="58" width="70" height="233" fill="#74ccf4" />
        </svg>
    </div>

</div>

In theory you could update the second bottle using only one event listener, but I didn't want to modifiy your code more than necessary. When you can, don't add 2 listeners to the same input when you can put your code in a single one, it makes the code easier to read

Also your function addBottle2 is not properly named, as it doesn't add a second bottle, it just sets the listener to it, and then shouldn't be called twice.

You have also minor optimisations that can be done. Note also that as it is, the code won't support different size bottles, as the function uses the values calculated for the first bottle's height

Kaddath
  • 5,933
  • 1
  • 9
  • 23
  • I wish you would, since "addBottle2" is not a great name for the function. Also I would do `bot2.hidden = inputWaterLevel.value <= 100` for dryness sake – mplungjan Jun 26 '23 at 09:18
  • 1
    @mplungjan yes I agree with you, even if IMO these points should be more suitable to Code Review SE. I could have added dynamic bottle creation to make the code generic, but judging the level of the question, I wanted the OP to be able to compare both codes easily to see which changes make it work, adding an edit though – Kaddath Jun 26 '23 at 09:26
  • @Kaddath thanks a lot, maybe you know where to look for similar examples of dynamic code, such a solution would also be interesting for me, because I want to add 6 bottles – Марта Jun 26 '23 at 09:44
  • @Марта I am a bit out of time right now, I'll try to edit when I have time – Kaddath Jun 26 '23 at 09:50
1

First, don't refer to the element using its id directly as an variable. See this post. Always use proper method to get the element from the DOM.

Second, the addBottle2 function is redundant.

inputWaterLevel.id = 'inpId';
let inpId = document.getElementById('inpId')

These two lines are redundant because you have already get the input element in inputWaterLevel. Changing its id and get it again by id do nothing useful. Just use it as is.

inpId.addEventListener("input", () => {...

This is also redundant because you have already added the event listener updateWaterLevel. You can simply add code in updateWaterLevel

let inputWaterLevel = document.querySelector('[name=water-level]');
//get tubLiquid properly
let tubeLiquid = document.getElementById("tubeLiquid");
let heightLiquid = +tubeLiquid.getAttribute('height');
let yLiquid = +tubeLiquid.getAttribute('y');
let liquidPercent = heightLiquid/100;

//duplicate for second bottle, unless assuming two bottle always have the same size hardcoded.
let tubeLiquid2 = document.getElementById("tubeLiquid2");
let heightLiquid2 = +tubeLiquid2.getAttribute('height');
let yLiquid2 = +tubeLiquid2.getAttribute('y');
let liquidPercent2 = heightLiquid2/100;
let bot2 = document.getElementById("bot2");

inputWaterLevel.addEventListener('input', updateWaterLevel);

function updateWaterLevel() {
  let value = +this.value;
  
  let height = liquidPercent * value;
  let y = yLiquid + (heightLiquid-height);
  
  tubeLiquid.setAttribute('height', liquidPercent * value );
  tubeLiquid.setAttribute('y', y );

  //bottle2
  if(value > 100)
  {
    let value2 = value - 100;
    let height2 = liquidPercent2 * value2;
    let y2 = yLiquid2 + (heightLiquid2-height2);

    tubeLiquid2.setAttribute('height', liquidPercent2 * value2 );
    tubeLiquid2.setAttribute('y', y2 );
    
    bot2.style.display = "block";
  }  
  else
  {
    bot2.style.display = "none";
  }
}
.cont{
     display: flex;
 }

 #bot2{
     display: none;
 }
<p><input type="number" min="0" max="200" value="100" name="water-level" /></p>

<div class="cont">
    <div class="bot1">
        <svg viewBox="0 0 300 300" width="150">
            <clipPath id="clip">
                <rect x="118" y="68" width="64" height="228" rx="20" ry="20" />
            </clipPath>
            <image width="300" xlink:href="https://i.ibb.co/HGFQYTj/bottle.png" />
            <rect clip-path="url(#clip)" id="tubeLiquid" x="115" y="58" width="70" height="233" fill="#74ccf4" />
        </svg>
    </div>

    <div id="bot2">
        <svg viewBox="0 0 300 300" width="150">
            <clipPath id="clip2">
                <rect x="118" y="68" width="64" height="228" rx="20" ry="20" />
            </clipPath>
            <image width="300" xlink:href="https://i.ibb.co/HGFQYTj/bottle.png" />
            <rect clip-path="url(#clip2)" id="tubeLiquid2" x="115" y="58" width="70" height="233" fill="#74ccf4" />
        </svg>
    </div>

</div>
Ricky Mo
  • 6,285
  • 1
  • 14
  • 30
1

You don't need a second function to achieve that.

You can check: when the input value is greater than 100, the height and position of tubeLiquid2 are calculated based on the remaining value (value - 100), and the bot2 div is displayed.

let inputWaterLevel = document.querySelector('[name=water-level]');
let tubeLiquid = document.getElementById('tubeLiquid');
let tubeLiquid2 = document.getElementById('tubeLiquid2');
let bottle2 = document.getElementById('bot2');
let liquidPercent = tubeLiquid.getAttribute('height') / 100;
let yLiquid = +tubeLiquid.getAttribute('y')
let heightLiquid = +tubeLiquid.getAttribute('height');

inputWaterLevel.addEventListener('input', updateWaterLevel);

function updateWaterLevel() {
  let value = +this.value;

  if (value <= 100) {
    let height = liquidPercent * value;
    let y = yLiquid + (heightLiquid - height);

    tubeLiquid.setAttribute('height', height);
    tubeLiquid.setAttribute('y', y);
    tubeLiquid2.setAttribute('height', 0);
    bottle2.style.display = 'none';
  } else {
    let height1 = liquidPercent * 100;
    let y1 = yLiquid + (heightLiquid - height1);

    let height2 = liquidPercent * (value - 100);
    let y2 = yLiquid + (heightLiquid - height2);

    tubeLiquid.setAttribute('height', height1);
    tubeLiquid.setAttribute('y', y1);
    tubeLiquid2.setAttribute('height', height2);
    tubeLiquid2.setAttribute('y', y2);
    bottle2.style.display = 'block';
  }
}
.cont{
     display: flex;
 }

 #bot2{
     display: none;
 }
<p><input type="number" min="0" max="200" value="100" name="water-level" /></p>

<div class="cont">
  <div id="bot1">
    <svg viewBox="0 0 300 300" width="150">
      <clipPath id="clip">
        <rect x="118" y="68" width="64" height="228" rx="20" ry="20" />
      </clipPath>
      <image width="300" xlink:href="https://i.ibb.co/HGFQYTj/bottle.png" />
      <rect clip-path="url(#clip)" id="tubeLiquid" x="115" y="58" width="70" height="233" fill="#74ccf4" />
    </svg>
  </div>

  <div id="bot2">
    <svg viewBox="0 0 300 300" width="150">
      <clipPath id="clip2">
        <rect x="118" y="68" width="64" height="228" rx="20" ry="20" />
      </clipPath>
      <image width="300" xlink:href="https://i.ibb.co/HGFQYTj/bottle.png" />
      <rect clip-path="url(#clip2)" id="tubeLiquid2" x="115" y="58" width="70" height="233" fill="#74ccf4" />
    </svg>
  </div>
</div>

UPDATE

In the example below, we create a dynamic water bottles filler.

  1. We calculate the numBottles variable using Math.max(Math.ceil(value / 100), 1). This calculation ensures that at least one bottle is present, even when the input value is 0. We use Math.ceil(value / 100) to round up the division result to the nearest integer, indicating the number of bottles required. However, we take the maximum of this result and 1 to ensure that there's always at least one bottle.

  2. The for loop creates the required number of div elements, each representing a bottle. The loop runs from i = 1 to i <= numBottles. We set the id of each div element dynamically using bottleDiv.id = bot${i}. Inside the loop, we create an SVG element for each bottle using a similar structure to the original code.

  3. The second for loop iterates through the tubeLiquidElements array, which contains all the liquid rectangles for each bottle. Inside the loop, we calculate the bottleValue for each bottle by subtracting (i * 100) from the input value. This adjusted value ensures that each bottle represents the correct amount of water without repeating the empty/filled pattern at multiples of 100.

  4. To prevent negative heights, we added Math.max(bottleValue, 0) when calculating the height of the liquid. This makes sure that the height is always 0 or greater, even if the bottleValue is negative (for values less than 100 in subsequent bottles).

In short, we ensure that the first bottle is always present, even when the input reaches 0, and the subsequent bottles accurately represent the water level based on the input value. The maximum number of bottles here is 6 (as you asked for in the comments section), since the attribute max of the input is 600.

let inputWaterLevel = document.querySelector('[name=water-level]');
let container = document.querySelector('.cont');

inputWaterLevel.addEventListener('input', updateWaterLevel);

function updateWaterLevel() {
    // Get input value
    let value = +this.value;
    // Calculate the number of bottles required, minimum 1
    let numBottles = Math.max(Math.ceil(value / 100), 1); 
    // Clear the container
    container.innerHTML = ''; 

    for (let i = 1; i <= numBottles; i++) {
        let bottleDiv = document.createElement('div');
        bottleDiv.id = `bot${i}`;
        bottleDiv.innerHTML = `
        <svg viewBox="0 0 300 300" width="150">
            <clipPath id="clip${i}">
            <rect x="118" y="68" width="64" height="228" rx="20" ry="20" />
            </clipPath>
            <image width="300" xlink:href="https://i.ibb.co/HGFQYTj/bottle.png" />
            <rect clip-path="url(#clip${i})" class="tubeLiquid" x="115" y="58" width="70" height="233" fill="#74ccf4" />
        </svg>`;
        container.appendChild(bottleDiv);
    }

    let tubeLiquidElements = document.querySelectorAll('.tubeLiquid');
    let liquidPercent = tubeLiquidElements[0].getAttribute('height') / 100;
    let yLiquid = +tubeLiquidElements[0].getAttribute('y');
    let heightLiquid = +tubeLiquidElements[0].getAttribute('height');

    for (let i = 0; i < numBottles; i++) {
        let tubeLiquid = tubeLiquidElements[i];
        // Adjusted value for each bottle
        let bottleValue = (value - (i * 100)); 
        let height = (bottleValue >= 100) ? liquidPercent * 100 : liquidPercent * bottleValue;
        let y = yLiquid + (heightLiquid - height);

        tubeLiquid.setAttribute('height', height);
        tubeLiquid.setAttribute('y', y);
    }
}
.cont {
    display: flex; 
    flex-wrap: wrap;
}
<input type="number" min="0" max="600" value="100" name="water-level" />

<div class="cont">
  <div class="bottle">
    <svg viewBox="0 0 300 300" width="150">
      <clipPath id="clip">
        <rect x="118" y="68" width="64" height="228" rx="20" ry="20" />
      </clipPath>
      <image width="300" xlink:href="https://i.ibb.co/HGFQYTj/bottle.png" />
      <rect clip-path="url(#clip)" class="tubeLiquid" x="115" y="58" width="70" height="233" fill="#74ccf4" />
    </svg>
  </div>
</div>
001
  • 2,019
  • 4
  • 22
  • thanks a lot, I would also like to ask, if I need 6 bottles, is it appropriate to use that example to add for them such conditions? – Марта Jun 26 '23 at 13:51
  • @Марта Check the update section please. – 001 Jun 26 '23 at 16:45
  • @thanks a lot!! its very useful for me – Марта Jun 26 '23 at 16:58
  • That looks like the crap solution chatGPT would give when it cannot figure it out. It is not recommended to add and remove from the DOM like this. It adds and removes from the dom for EVERY update of the input. – mplungjan Jun 27 '23 at 06:15
1

Water Level Task

Create a second function to manage the water level of the second bottle. Lets call the new function updateWaterLevel2(). We will use a very similar logic to updateWaterLevel(), however it will operate on tubeLiquid2 element instead.

In updateWaterLevel I will add a conditional check when the value goes over 100 and call updateWaterLevel2() with the excess amount. Inside the updateWaterLevel2() function, we will adjust the height and y attributes of tubeLiquid2 just like in tubeLiquid.

Also you need to select the tubeLiquid2 element from the DOM and get the initial height and y variable values. Those values are needed to calculate the new height and y positions of the liquid inside the second bottle.

// Select elements from the DOM
let inputWaterLevel = document.querySelector('[name=water-level]');
let tubeLiquid = document.querySelector('#tubeLiquid');
let tubeLiquid2 = document.querySelector('#tubeLiquid2');

// Get the initial attributes from the first SVG element
let heightLiquid = +tubeLiquid.getAttribute('height');
let yLiquid = +tubeLiquid.getAttribute('y');
let liquidPercent = heightLiquid / 100;

// Get the initial attributes from the second SVG element
let heightLiquid2 = +tubeLiquid2.getAttribute('height');
let yLiquid2 = +tubeLiquid2.getAttribute('y');
let liquidPercent2 = heightLiquid2 / 100;

// Add an event listener to your input field
inputWaterLevel.addEventListener('input', updateWaterLevel);

// This is the function to update the water level inside the first bottle
function updateWaterLevel() {
  let value = +this.value;

  let height = liquidPercent * value;
  let y = yLiquid + (heightLiquid - height);

  // Set new attributes to first SVG element
  tubeLiquid.setAttribute('height', liquidPercent * value)
  tubeLiquid.setAttribute('y', y)

  // Check if value is greater than 100 and update second bottle value
  if (value > 100) {
    updateWaterLevel2(value - 100);
  } else {
    tubeLiquid2.setAttribute('height', 0);
  }
}

// The function to update water level inside the second bottle
function updateWaterLevel2(value) {
  let height = liquidPercent2 * value;
  let y = yLiquid2 + (heightLiquid2 - height);

  // Set the new attributes to the second SVG element
  tubeLiquid2.setAttribute('height', liquidPercent2 * value)
  tubeLiquid2.setAttribute('y', y)
}

// Function to show or hide the second bottle depending on the input
let addBottle2 = () => {
  inputWaterLevel.id = 'inpId';
  let inpId = document.getElementById('inpId')
  inpId.addEventListener("input", () => {
    if (inpId.value > 100) {
      document.getElementById("bot2").style.display = 'block';
    } else {
      document.getElementById("bot2").style.display = 'none';
    }
  });
}

// Call function to initialize bottle display
addBottle2();
.cont {
  display: flex;
}

#bot2 {
  display: none;
}
<p><input type="number" min="0" max="200" value="100" name="water-level" /></p>

<div class="cont">
  <div class="bot1">
    <svg viewBox="0 0 300 300" width="150">
            <clipPath id="clip">
                <rect x="118" y="68" width="64" height="228" rx="20" ry="20" />
            </clipPath>
            <image width="300" xlink:href="https://i.ibb.co/HGFQYTj/bottle.png" />
            <rect clip-path="url(#clip)" id="tubeLiquid" x="115" y="58" width="70" height="233" fill="#74ccf4" />
        </svg>
  </div>

  <div id="bot2">
    <svg viewBox="0 0 300 300" width="150">
            <clipPath id="clip2">
                <rect x="118" y="68" width="64" height="228" rx="20" ry="20" />
            </clipPath>
            <image width="300" xlink:href="https://i.ibb.co/HGFQYTj/bottle.png" />
            <rect clip-path="url(#clip2)" id="tubeLiquid2" x="115" y="58" width="70" height="233" fill="#74ccf4" />
        </svg>
  </div>

</div>
mplungjan
  • 169,008
  • 28
  • 173
  • 236
AztecCodes
  • 1,130
  • 7
  • 23
1

Here is a script that takes the value and max from the input to create bottlesOnTheWall bottles and shows and fills them until max is reached

  1. It creates all the bottles and shows and hides them when needed
  2. It does update ALL the bottles' height since we can enter any amount
  3. it does NOT add or removed the SVGs from the DOM more than once

To change from 6 to 8 bottles, change the max to 800

const inputWaterLevel = document.querySelector('[name=water-level]');
const div = document.querySelector(".cont");
const svgContainer = document.querySelector('.bot');
const bottlesOnTheWall = Math.floor(inputWaterLevel.max / inputWaterLevel.value);
const bottleCapacity = inputWaterLevel.value;
const totalAmount = inputWaterLevel.max;
const firstTube = svgContainer.querySelector(".tubeLiquid");
const fullHeight = +firstTube.getAttribute('height');
const fullY = +firstTube.getAttribute('y');
const liquidPercent = fullHeight / 100;
console.log(bottlesOnTheWall, bottleCapacity)
const cloneSVG = (svgContainer, idx) => {
  // clone the svgContainer
  const clone = svgContainer.cloneNode(true);
  // create id
  const id = `clip-${idx}`;
  clone.querySelector('clipPath').id = id;
  // set the rect's clip-path url to the  id
  const rect = clone.querySelector('.tubeLiquid');
  rect.setAttribute('clip-path', `url(#${id})`);
  return clone;
};
Array.from({
  length: bottlesOnTheWall
}).forEach((_, i) => {
  const svgClone = cloneSVG(svgContainer, ++i); // creates bottle id=1, id=2
  // then append svgClone to the .cont 
  div.appendChild(svgClone);
});

const allBottles = document.querySelectorAll(".bot");

const fillBottles = () => {
  let totalAmount = +inputWaterLevel.value
  // Number of full bottles
  let fullBottles = Math.floor(totalAmount / bottleCapacity);
  // Calculate the remainder for the last bottle
  let remainder = totalAmount % bottleCapacity;
  if (fullBottles >= bottlesOnTheWall) {
    fullBottles = bottlesOnTheWall;
    remainder = totalAmount - (bottleCapacity * (bottlesOnTheWall - 1));
  }
  let height = liquidPercent * remainder;
  let y = fullY + (fullHeight - height);
  allBottles.forEach((bottle, i) => {
    const tubeLiquid = bottle.querySelector(".tubeLiquid");
    const show = i == 0 || i < fullBottles || (i === (fullBottles) && remainder); // show or not
    bottle.hidden = !show;
    tubeLiquid.setAttribute('height', (i < fullBottles) ? fullHeight : height)
    tubeLiquid.setAttribute('y', (i < fullBottles) ? fullY : y)
  });
};

inputWaterLevel.addEventListener('input', fillBottles)
fillBottles();
.cont {
  display: flex;
}
<p><input type="number" min="0" max="600" value="100" name="water-level" /></p>

<div class="cont">
  <div class="bot">
    <svg viewBox="0 0 300 300" width="150">
      <clipPath id="clip-0">
        <rect x="118" y="68" width="64" height="228" rx="20" ry="20" />
      </clipPath>
      <image width="300" xlink:href="https://i.ibb.co/HGFQYTj/bottle.png" />
      <rect clip-path="url(#clip-0)" class="tubeLiquid" x="115" y="58" width="70" height="233" fill="#74ccf4" />
    </svg>
  </div>
</div>
mplungjan
  • 169,008
  • 28
  • 173
  • 236