DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

SparkedScience
SparkedScience

Posted on

Carded, but with Flashes

I'm in some kind of mood tonight. I've been banging my head against a wall of code (New favorite phrase) all night with Java, so naturally we should talk about JavaScript.

The project

We, being PenStat (links in the bottom), were tasked with creating a flash card element. We took to recreating the original design as close as possible. Here is the reference:
Original Flashcard
And here is our final product:
Finished Flashcard
There are a few key differences...

  • We implemented Simple Colors. This allows us to implement a simple dark mode, and the library was specifically created by the ELMSLN to be compliant with contrast ratios for accessibility standards.
  • Our images are customizable. They also rely on loremflickr. I worked on this section in the beginning. If the overall tag isn't given an image, it will pull an image from loremflickr as a placeholder.
  • We also added an option for the card to read the question out loud. More info on this portion can be found here.

Fun, right?

What I did

I briefly touched on the image portion, so we'll cover that further. But I also worked on an addition to the card that allows users to add multiple cards in one element. I'll talk about both of those now.

Images

I initially worked on getting loremflickr functional. The final product is very simple. We use <img src="${this.imgSrc}" alt=""/> if the tag is supplied an image, and <img src="https://loremflickr.com/320/240/${this.imgKeyword}?lock=1" alt=""/> for default/keyword images. If there isn't a keyword, the element sticks in a placeholder of "grey box."

Flash Card Array

The biggest wall of code I worked on in the last week before our deadline. Flash Card Array
This is my final result. It is a logic heavy element, with very little happening on the front end. I will show you the best bits.

The Best Bits

01010100011010000110010100100000010000100110010101110011 011101000010000001000010011010010111010001110011
I'm kidding. I told you, I'm in some kind of mood tonight.
The two functions doing all of the work are getData() and formatEl(). Respectively, they get the data and create the different flash-card tags. Let's look at get data first. Here is the schema to use <flash-card-set>:

    <flash-card-set>
    <ul>
      <li>
        <p slot="front">What is strawberry in Spanish?</p>
        <p slot="back">fresa</p>
        <p slot="image">https://loremflickr.com/320/240/strawberry</p>
      </li>
      <li>
        <p slot="image">https://loremflickr.com/320/240/food</p>
        <p slot="attributes">speak</p>
        <p slot="front">What is food in Spanish?</p>
        <p slot="back">comida</p>
      </li>
      <li>
        <p slot="back">persona</p>
        <p slot="front">What is people in Spanish?</p>
        <p slot="image">https://loremflickr.com/320/240/manequin</p>
        <p slot="attributes">speak dark</p>
      </li>
    </ul>
  </flash-card-set>
Enter fullscreen mode Exit fullscreen mode

It doesn't matter the order of the slots, but it relies on using a <ul> element with <li> and named slots. (I talk about named slots in my series, go check that out for a refresher.) The <ul> is a container for all of the <li>, and each <li> is a separate card. Now for the fun part: code.

getData() {
    const slotData2 = this.shadowRoot
      .querySelector(`slot`).assignedNodes({ flatten: true })[1].childNodes;
    const questionData = ['','','',''];
    // eslint-disable-next-line no-plusplus
    for (let i = 0; i < slotData2.length; i++) {
      if (i % 2 === 1) {
        // eslint-disable-next-line no-plusplus
        for (let j = 0; j < slotData2[i].childNodes.length; j++) {
          if (j % 2 === 1) {
            const {slot} = slotData2[i].childNodes[j];
            if (slot === 'front') {
              questionData[0] = slotData2[i].childNodes[j].innerHTML;
            }
            if (slot === 'back') {
              questionData[1] = slotData2[i].childNodes[j].innerHTML;
            }
            if (slot === 'image') {
              questionData[2] = slotData2[i].childNodes[j].innerHTML;
            }
            if (slot === 'attributes') {
              questionData[3] = slotData2[i].childNodes[j].innerHTML;
            }
          }
        }
        // eslint-disable-next-line no-plusplus
        for (let k = 0; k < questionData.length; k++) {
          this.questions.push(questionData[k]);
        }
        // eslint-disable-next-line no-plusplus
        for (let l = 0; l < 4; l++) {
          questionData[l] = '';
        }
      }
    }
  }
Enter fullscreen mode Exit fullscreen mode

Fun, isn't it? I'll go line by line.

const slotData2 = this.shadowRoot
      .querySelector(`slot`).assignedNodes({ flatten: true })[1].childNodes;
Enter fullscreen mode Exit fullscreen mode

I found this syntax after trial and error. We get the slot element in the render function of the element, and then get all of its data, aka the <ul> element. The querySelector renders weird, so we grab the second position in the array and get the child nodes, or all of the <li> elements.

const questionData = ['','','',''];
Enter fullscreen mode Exit fullscreen mode

The specifications of the flash-card-set only allow for four items, the question, answer, image data, and any tag properties; such as speak or dark. Each slot in the array holds one of these values.
// eslint-disable-next-line no-plusplus eslint hates fun.

for (let i = 0; i < slotData2.length; i++) {
      if (i % 2 === 1) {
Enter fullscreen mode Exit fullscreen mode

We loop though each <li> node in the slot. With how slots, querySelector, and lists work, we have to call the odd positions in the array. The even positions are just blank lines.

for (let j = 0; j < slotData2[i].childNodes.length; j++) {
Enter fullscreen mode Exit fullscreen mode


Another loop!!!!!!! And I broke dev.to. This loop is to loop through the elements in each <li> element, aka the named slot elements. We also need the modulus operator again for the same reason as before.

const {slot} = slotData2[i].childNodes[j];
Enter fullscreen mode Exit fullscreen mode

This gets the name of the slot for comparison later.

if (slot === 'front') {
              questionData[0] = slotData2[i].childNodes[j].innerHTML;
            }
Enter fullscreen mode Exit fullscreen mode

It is now later. Each piece of the spec; front, back, image, and attributes; has it's own if block.

slotData2[i].childNodes[j].innerHTML
Enter fullscreen mode Exit fullscreen mode

This line gets the innerHTML, or the data of the current slot in the j loop, from the tag of the current card in the i loop. I won't lie, a lot of trial and error went into these lines. It's added to the array in order.

for (let k = 0; k < questionData.length; k++) {
          this.questions.push(questionData[k]);
        }
Enter fullscreen mode Exit fullscreen mode

We have a property, questions, for all of the question data of all of the cards in the set. The order of questionData is important since I used integer indexing later on. This loop just adds the data from the j loop into the questions property.

for (let l = 0; l < 4; l++) {
          questionData[l] = '';
        }
Enter fullscreen mode Exit fullscreen mode

Reset the array for the next card.
And now to create the elements.

formatEl(number) {
    // create a new element
    const el = document.createElement('flash-card');
    el.setAttribute('id', `card${number}`);
    if (number !== 0) {
      el.className = 'hidden';
    }
    // add the text
    el.innerHTML = `
      <p slot="front">${arguments[1]}</p>
      <p slot="back">${arguments[2]}</p>`;
    // eslint-disable-next-line prefer-rest-params
    el.setAttribute('img-src', arguments[3]);
    // eslint-disable-next-line prefer-rest-params
    if (arguments[4].includes('speak')) {
      el.setAttribute('speak', '');
    }
    // eslint-disable-next-line prefer-rest-params
    if (arguments[4].includes('dark')) {
      el.setAttribute('dark', '');
    }
    // eslint-disable-next-line prefer-rest-params
    if (arguments[4].includes('back')) {
      el.setAttribute('back', '');
    }
    // append it to the parent
    this.shadowRoot.querySelector('#content').appendChild(el);
  }
Enter fullscreen mode Exit fullscreen mode

More code, yay.

const el = document.createElement('flash-card');
    el.setAttribute('id', `card${number}`);
Enter fullscreen mode Exit fullscreen mode

We create a new flash-card element and give it an ID.

if (number !== 0) {
      el.className = 'hidden';
    }
Enter fullscreen mode Exit fullscreen mode

This is for the rendering. Everything except the first element is hidden by default. We have css classes for hidden and visible, which another function toggles between.

el.innerHTML = `
      <p slot="front">${arguments[1]}</p>
      <p slot="back">${arguments[2]}</p>`;
Enter fullscreen mode Exit fullscreen mode

Each question has to have a question and answer, so we can hardcode these two arguments. JavaScript has a keyword, arguments, in each method. It's an array of arguments that were supplied in the method call. Very useful.

el.setAttribute('img-src', arguments[3]);
Enter fullscreen mode Exit fullscreen mode

We can also hardcore this since a blank value passed to the image-prompt will use the default value.

 if (arguments[4].includes('speak')) {
      el.setAttribute('speak', '');
    }
    // eslint-disable-next-line prefer-rest-params
    if (arguments[4].includes('dark')) {
      el.setAttribute('dark', '');
    }
    // eslint-disable-next-line prefer-rest-params
    if (arguments[4].includes('back')) {
      el.setAttribute('back', '');
    }
Enter fullscreen mode Exit fullscreen mode

Each of these if statements checks the attributes section of the array. If it contains any of the key words, then that attribute is set in the flash-card element.

this.shadowRoot.querySelector('#content').appendChild(el);
Enter fullscreen mode Exit fullscreen mode

There is a div in the render function to house all of the cards.
It's a lot of code, but it's what I'm most proud of from this project.

Final schtuff

Go check us out in the links below.

Links

GitHub: https://github.com/PenStat
NPM: https://www.npmjs.com/org/penstat2
Flash Card project:

Top comments (0)

Why You Need to Study Javascript Fundamentals

The harsh reality for JS Developers: If you don't study the fundamentals, you'll be just another β€œCoder”. Top learnings on how to get to the mid/senior level faster as a JavaScript developer by Dragos Nedelcu.