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:
And here is our final product:
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.
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>
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] = '';
}
}
}
}
Fun, isn't it? I'll go line by line.
const slotData2 = this.shadowRoot
.querySelector(`slot`).assignedNodes({ flatten: true })[1].childNodes;
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 = ['','','',''];
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) {
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++) {
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];
This gets the name of the slot for comparison later.
if (slot === 'front') {
questionData[0] = slotData2[i].childNodes[j].innerHTML;
}
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
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]);
}
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] = '';
}
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);
}
More code, yay.
const el = document.createElement('flash-card');
el.setAttribute('id', `card${number}`);
We create a new flash-card element and give it an ID.
if (number !== 0) {
el.className = 'hidden';
}
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>`;
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]);
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', '');
}
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);
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)