99 Bottles of Beer on the Wall : Frontend Coding Exercises

This is a companion discussion topic for the original entry at https://www.kirupa.com/codingexercises/99bottles_beer_wall.htm

If you really want to annoy your co- workers… :grin:
This one does it with the SpeechSynthesis API…

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
  <style> body {margin: 0; padding: 1rem;} </style>
  <script>
        let position;
        const player = new SpeechSynthesisUtterance()
        player.addEventListener('pause', e => position = e.charIndex)
        const play = function() {
            if (speechSynthesis.paused && speechSynthesis.speaking) return speechSynthesis.resume();
            if (speechSynthesis.speaking) return;
            let str = '';
            for (let i = 99; i; i--) {
                str += `
                ${i} bottle${i == 1 ? ``: 's'} of beer on the wall
                ${i} bottle${i == 1 ? ``: 's'} of beer
                take 1 down
                pass it around
                ${i - 1} bottle${i-1 == 1 ? ``: 's'} of beer on the wall`
            }
            player.text = str;
            player.rate = 2
            speechSynthesis.speak(player)
        }
        const pause = function() {
            if (speechSynthesis.speaking) speechSynthesis.pause()
        }
        const rewind = function() {
            speechSynthesis.resume()
            speechSynthesis.cancel()
        }
  </script>
</head>
<body>
  <button onclick="play()">Play</button>
  <button onclick="pause()">Pause</button>
  <button onclick="stop()">Rewind</button>
</body>
</html>
2 Likes

That is really nice! You beat me to a future exercise where I was going to focus on the SpeechSynthesis API :stuck_out_tongue:

You also got the badge as well:

98 bottles of beer on the wall, 98 bottles of beer.
Take one down and pass it around, 97 bottles of beer on the wall.

97 bottles of beer on the wall, 98 bottles of beer.
Take one down and pass it around, 97 bottles of beer on the wall.

96 bottles of beer on the wall, 98 bottles of beer.
Take one down and pass it around, 97 bottles of beer on the wall.

^ Got some copy-pasta errors in there :wink:

1 Like

Web component with a slider:

class BottlesOfBeer extends HTMLElement {

  static DEFAULT_COUNT = 99;
  static get observedAttributes() {
    return ["count"];
  }

  #output1 = null;
  #output2 = null;
  #slider = null;

  get count() {
    return +this.getAttribute("count") || BottlesOfBeer.DEFAULT_COUNT;
  }
  set count(value) {
    this.setAttribute("count", +value || "");
  }

  constructor() {
    super();
    
    const shadow = this.attachShadow({
      mode: "open"
    });
    shadow.innerHTML = `
    <style>
    #wrap {
      display: flex;
      flex-direction: column;
    }
    </style>
    <div id="wrap">
      <output id="line1"></output>
      <output id="line2"></output>
      <input type="range" value="0">
    <div>
    `;

    this.#output1 = shadow.querySelector("#line1");
    this.#output2 = shadow.querySelector("#line2");
    this.#slider = shadow.querySelector("input");
    this.#slider.oninput = () => this.render();
  }

  connectedCallback() {
    this.updateRange();
  }

  attributeChangedCallback(name, oldValue, newValue) {
    switch (name) {
      case "count": {
        this.updateRange();
      }
    }
  }

  getBottlesText(count) {
    const s = count === 1 ? "" : "s";
    return count ? `${count} bottle${s}` : "no more bottles";
  }

  up1st(text) {
    return text[0].toUpperCase() + text.slice(1);
  }

  getTexts(count) {
    const texts = [];
    const nextCount = count - 1;

    let countText = this.getBottlesText(count);
    texts.push(`${this.up1st(countText)} of beer on the wall, ${countText} of beer.`);

    countText = this.getBottlesText(count ? nextCount : this.count);
    texts.push(count
      ? `Take one down and pass it around, ${countText} of beer on the wall.`
      : `Go to the store and buy some more, ${countText} of beer on the wall.`);

    return texts;
  }

  updateRange() {
    this.#slider.max = this.count;
    this.render();
  }

  render() {
    const renderCount = this.count - this.#slider.value;
    [this.#output1.value, this.#output2.value] = this.getTexts(renderCount);
  }
}

customElements.define("bottles-of-beer", BottlesOfBeer);

Usage

<bottles-of-beer></bottles-of-beer>
<!-- With optional count attribute -->
<bottles-of-beer count="99"></bottles-of-beer>

1 Like

Nice, I was thinking about doing components for the button but I didn’t want to jam the thread with code… :slightly_smiling_face:

Its good to promote WC so maybe I’ll do all of the rest of my posts with WC…

Its also good to see how others do WC.

I don’t use template literals/ lit, I have a function that I pass in an array and it returns a docFrag with the tree. To update I iterate over both the array and the component…

I do often feel dirty using innerHTML, but it is the quickest way to get some DOM going in the web component (and the clearest way to see the DOM structure). And with the right editor plugins, you can get code coloring and code hints in those template strings to help keep you from making mistakes and make editing easier.

HTML templates exist, but they’re a pain to work with, having to include them in the HTML and then dig them back out again.

I’d like to think if we still had E4X it would work as a great way to represent DOM in JS. I can’t remember, though, if XML elements counted as DOM Nodes. Probably not so you’d still need some kind of parsing step to go from the XML to the DOM. But who needs E4X when we now have JSX? … all the people that don’t want to have to deal with a build step, that’s who :stuck_out_tongue_winking_eye: .

1 Like

I’m not really that concerned about innerHTML because I don’t usually do cross origin (maybe I should be worried about man in the middle :thinking:)

I just do it for performance of the initial render… it’s about 4x faster to clone a node than createElement() or innerHTML =…

It makes no performance difference for a few components but a list of 50+ items/ components has an effect. :slightly_smiling_face:

The syntax is not too bad e.g.:

static template = [
        { lvl: 0, tag: "my-custom", props: [ ] },
        { lvl: 1, tag: "SECTION", props: [{ prop: 'classList', key: 'class' } ] },
        { lvl: 2, tag: 'H1', props: [ ] }
        { lvl: 3, tag: 'textNode', key: 'text' },
]

static html = build_Template(myCustom.template)
1 Like

Yikes! Let me fix that in a few moments :sweat_smile:

You could do something similar with innerHTML and a string too. That can be lazily parsed into a template element and that template can be used to clone nodes into instances. In my component, moving the shadow DOM code to a static utility for this could look something like:

  static #template = null;
  static build(instance) {
  	if (!this.#template) {
      this.#template = document.createElement("template");
      this.#template.innerHTML = `
      <style>
      #wrap {
        display: flex;
        flex-direction: column;
      }
      </style>
      <div id="wrap">
        <output id="line1"></output>
        <output id="line2"></output>
        <input type="range" value="0">
      <div>
      `;
    }
    const shadow = instance.attachShadow({ mode: "open" });
    shadow.appendChild(this.#template.content.cloneNode(true));
    return shadow;
  }

This wouldn’t work if you wanted to include variables in the markup, but if you’re concerned about performance, you’d want to handle those through the DOM APIs anyway.

1 Like

That’s sweet, I didn’t realize you could pass in an instance into a static method (I’m a novice at OO :smile:)

I decided to make a very basic solution :stuck_out_tongue:

for (let i = 99; i >= 0; i--) {
  if (i >= 2) {
    console.log(`${i} bottles of beer on the wall, ${i} bottles of beer.`);
    console.log(`Take one down and pass it around, ${i - 1} bottles of beer on the wall.
    `);
  }

  if (i == 1) {
    console.log(`1 bottle of beer on the wall, 1 bottle of beer.`);
    console.log(`Take one down and pass it around, no more bottles of beer on the wall.
    `);
  }

  if (i == 0) {
    console.log(`No more bottles of beer on the wall, no more bottles of beer.`);
    console.log(`Go to the store and buy some more, 99 bottles of beer on the wall.
    `);
  }
}

I went ahead and recorded a video of it as well:

:crazy_face:

1 Like

This was fun!

bottles = c(paste(99:2, "bottles"), "1 bottle")
bottles2 = c(bottles, "no more bottles")
bottles = c(bottles, "No more bottles")
bottles3 = c(paste("Take one down and pass it around,", bottles2[-1], "of beer on the wall.\n\n"),
             "Go to the store and buy some more, 99 bottles of beer on the wall.")
cat(paste0(bottles, " of beer on the wall, ", bottles2, " of beer.\n",
           bottles3), sep = "")

1 Like

Award granted! :stuck_out_tongue:

1 Like

That was fun :slight_smile:
Here is my solution:

const numberOfBeers = 99;

function printLyrics(numberOfBeers) {
  for (beerCounter = numberOfBeers; beerCounter >= 0; beerCounter--) {
    if (beerCounter >= 2) {
      console.log(
        `${beerCounter} bottles of beer on the wall, ${beerCounter} bottles of beer.`
      );
      console.log(
        `Take one down and pass it around, ${
          beerCounter - 1
        } bottles of beer on the wall.`
      );
    } else if (beerCounter == 1) {
      console.log(
        `${beerCounter} bottle of beer on the wall, ${beerCounter} bottle of beer.`
      );
      console.log(
        `Take one down and pass it around, no more bottles of beer on the wall.`
      );
    } else {
      console.error(
        "No more bottles of beer on the wall, no more bottles of beer."
      );
      console.info(
        `Go to the store and buy some more, ${beerCounter} bottles of beer on the wall.`
      );
    }
  }
}

printLyrics(numberOfBeers);
2 Likes

Badge granted!

Glad you liked it :slight_smile: