30 days of JavaScript - Part 2 of 5

November 14, 2019

If you followed along with me on the journey through JS30 Challenge from the previous post I am happy to say I keep going! I did skipped a day or two in the past 10 challenges, but nobody is perfect, so do not judge 💪

Again, all my solutions are easily accessible on my CodePen in case you wanted to explore the context for some of these exercises in more detail.

I received feedback it wasn’t the easiest to follow my thought process in some of the exercises from Part 1, so I made sure to provide more explanation this time around:

Day 11: Video Player

For a person who has never done anything with HTML5 video before, I got excited even just by playing with HTML itself for this exercise. I mean.. check it out, doesn’t it look great already?

video player

But the further into this lesson, the more exciting it became. I learnt all about implementing a custom look for a video player. We hid all the default panels styling and built our own version of the features such as the basic control panel, scrubbing the video, changing the volume and speed, pause and play.

To start with, I learnt HTML5 video has a bunch of useful properties, such as the event listeners for ‘play’ and ‘pause’ and others:

💥 video.paused

💥 video.currentTime

💥 video.duration

💥 video.play()

💥 video.pause()

We followed three steps for implementing each of these features:

1. Retrieve our elements (player, video, buttons, progressBar etc.) with the query selectors, for example:

html

2. Build out the functionality

Our video needs to be able to play and pause on click. For that we built the toggle function: if video is playing, stop. If stopped, play.

For the skip function. We want to either rewind or forward the current playing time. Both of these buttons on our video player have a data-skip attribute of either -10 or 25, so we use that value in our next function:

                function skip() {
                    video.currentTime += parseFloat(this.dataset.skip)
                }

Similarly, in order to update the playback speed we add ‘change’ event listener onto each of the ranges and update it with the given value:

                function updateRange() {
                    video[this.name] = this.value;
                }

                ranges.forEach(range => range.addEventListener('change', updateRange));

3. Hook up event listeners to make our video player interactive

                const skipButtons = player.querySelectorAll('[data-skip]');

                skipButtons.forEach(button => button.addEventListener('click', skip))

                ranges.forEach(range => range.addEventListener('mousemove', updateRange))

Implementing progress bar ⭐️

When you play the video, the progress bar should be updating in real time.

video player

We can use the value of the flex-basis to track the progress of the video. The value of 0% means the video has not started yet,100% is equivalent to finished. Thus, we can calculate the progress at any given time by knowing how long is the video (video.duration) and how far we are right now (video.currentTime).

Here is our progress bar CSS:

                .progress {
                    flex:10;
                    position: relative;
                    display:flex;
                    flex-basis:100%;
                    height:5px;
                    transition:height 0.3s;
                    background:rgba(0,0,0,0.5);
                    cursor:ew-resize;
                }

                .progress__filled {
                    width:50%;
                    background:#ffc600;
                    flex:0;
                    flex-basis:50%;
                }

Flex-basis specifies the initial size of the flex item, before any available space is distributed according to the flex factors.

Video has a time update event that we can listen to, and once that’s emitted we can run our handleProgress function:

                video.addEventListener('timeupdate', handleProgress)

                function handleProgress() {
                    const progressPercentage = (video.currentTime / video.duration) * 100;
                    progressBar.style.flexBasis = `${progressPercentage}%`
                }

Scrub

If you click on the progress bar and play, the video itself should update as well.

Again, we listen to on click event on the video progress bar. The video will automatically update when the user clicks on the progress bar.

On click on the progress bar, the emitted event has a property of offsetX and that will give us the position we are looking for.

ProgressBar in turn has a property of offsetWidth that will give us the entire width of the bar.

When we multiply that value by the video duration, we will receive the video’s current time to scrub to:

                function scrub(e) {
                    const scrubPosition = (e.offsetX / progress.offsetWidth) * video.duration;
                    video.currentTime = scrubPosition;
                }

If a movie is one minute long, and you are 56% into its duration, then it will scrub to 33.6s of the video.

We also want to be able to update the video’s progress based on the mouse moves of dragging and dropping:

                progress.addEventListener('mousemove', (e) => mousedown && scrub(e));

In order to avoid the video trying to get updated on each mouse move, we can create a flag that is set to true only when the user clicks:

Is the user currently clicking? No.

                let mousedown = false;

When the user is clicking, we set it to true:

                progress.addEventListener('mousedown', () => mousedown = true);

And when they stop:

                progress.addEventListener('mouseup', () => mousedown = false);

We only scrub the video, when the user is clicking:

                progress.addEventListener('mousemove', () => {
                    if(mousedown) {
                        scrub()
                    }
                });

                progress.addEventListener('mousemove', (e) => mousedown && scrub(e));

Day 12: Key Sequence Detection

This was the cutest lesson ever. If you type the magic password sequence with your keyboard, you got rewarded with this:

unicorns

And all that is created with cornify_add() function from this unicorn tool I’ve never heard about it before, so thanks Wes for making my life better one unicorn at a time!

And just as a refresher for the use of splice method. It changes the contents of an array by removing or replacing existing elements and/or adding new elements in place:

splice method

Day 13: Slide in on scroll

With this lesson we are building the functionality to slide images on the screen whilst scrolling.

We are listening to scroll event. There is a small gotcha with this. The scroll event gets fired off too many times if we just ask window to listen for it all the time. If we log it to the console, we will notice the number growing really fast - even with just one quick scroll from top to the bottom of the page.

That’s why we use debounce to fix it and improve the performance. For more info on what debounce does and how to use it, I recommend a blog post by David Walsh on Medium.

For our purposes, we reduce the number of the times ‘scroll’ event is fired off by adding debounce to our listener:

                window.addEventListener('scroll', debounce(checkSlide));

                function debounce(func, wait = 20, immediate = true) {...}

We can make the event run only every so often, for example every 20ms in order to prevent any performance issues.

In order to slide in all the photos on the page as we scroll, we loop over every image:

                const sliderImages = document.querySelectorAll('.slide-in');

                sliderImages.forEach(sliderImage => {...});

We want each image to get shown when it reaches 50% of its height. In order to implement that, we need to know a few things:

  1. How far is the page being currently scrolled down? scrollY tells us how far at the top of the window we are scrolled down:

                window.scrollY
  2. How do we know how far at the bottom we are? we want to slide in the image when we are 50% through its height:

                window.scrollY + window.innerHeight - gives us the pixel level of where we are at the bottom of the viewport
    
                const slideInAt = (window.scrollY + window.innerHeight) - sliderImage.height / 2;

When we are past the image, we want to slide it out:

  1. Where is the image from the top of the page?

                sliderImage.offsetTop
  2. What is the bottom of the image?

                const imageBottom = sliderImage.offsetTop + sliderImage.height;
  3. When the image should show? i.e. in its 50% height

                const isHalfShown = slideInAt > sliderImage.offsetTop; 
  4. When did we scroll past the image? When its position at the bottom is less than the position of the window when scrolling:

                const isNotScrolledPast = window.scrollY < imageBottom;

Finally, we can introduce the logic to either show it or not, depending on the position of the image:

                if (isHalfShown && isNotScrolledPast) {
                    sliderImage.classList.add('active');
                } else {
                    sliderImage.classList.remove('active');
                }

CSS:

css to slide in/out

The images are 30% slid off the page, and hidden by opacity: 0:

images slid off

When we scroll 50% into their height, the images get the class active, and so they slide in to the page, opacity changes to 1. When we scroll past them, the active class gets removed, they slide off again. And next image shows on the screen the moment we reach a half of its height on the page.

I highly recommend looking at the pen for this particular solution.

Day14: Objects and Arrays

In this lesson, we are being reminded about the difference between a copy and reference. It is very important to understand how it works with different data structures, because it proves to be a common source of bugs.

We talk about three different structures:

1. Strings, numbers, booleans

For these data types, we copy by value since we do not have an access to the memory address only to the value.

When we assign variables to other variables using ’=’, we copy the value to the new variable. They are copied by value.

For example, if I create a variable (myDog2) from my original variable (myDog), when I reassign the value of the variable (myDog), the string itself will get changed, but the copied variable (myDog2) will stay the same:

I have a Siberian Husky.

I have two Siberian Huskies.

reference string

I change the first one to Akita.

I still have two dogs, but now I have an Akita and a Siberian Husky 😍😍

reference string

Changing one does not change the other. The variables have no relationship to each other.

2. Arrays

For Arrays we copy by reference.

When I have an original array (doggies), and create a copy of it by reassigning to another variable (puppies), and then overwrite one of the original array’s values with something else (doggies[1] = “Shiba Inu”), I will also affect the copied version of the array:

dogs

This happens because we created a reference to the original array, not a copy!

In order to create a copy (new place in memory), we can use one of the four following options:

I. slice() => const puppies2 = doggies.slice()

II. concat() => const puppies3 = [].concat(doggies);

III. ES6 spread operator => const puppies 4 = […doggies]

IV. Array.from() => const puppies 5 = Array.from(doggies);

All the four functions above will create a copy of the array, so if we change either the original array or the copy, one won’t affect the other.

puppies2

3. Objects

Object data type behaves the same way as array when it comes to copying. If we just reassign it to another variable, and then modify one of the values, both will get affected.

dogobjects

That is because the values of the objects are the memory addresses of those objects. That means we can modify values that sit in those memory addresses. Again, this is called copy by reference.

In order to create an immutable copy of an object, we should use:

I. Object.assign({}, person, {coolFactor: 10})

copy

This only works for the shallow copy - one level deep. So if you needed to go and change a property that is nested deeper than that, it won’t work.

II. Object Spread => {…bestDog}

Wes gives an alternative if you need to copy the object that deeply.

JSON.parse(JSON.stringify(wes))

deep copy

If you want to read more about this topic, I recommend a short post by Flavio on how to deep clone a JavaScript object.

Day 15: Local storage and Event Delegation

Persisting data with local storage.

In this challenge, we built a menu list that persist along with its state (whether the item on the list has been checked or not) even if the page is refreshed.

We start with just a simple html form that has two input elements, one to type the menu item and the other one to add it to the list on click:

menu

We start from declaring our variables by selecting them from our html (document.querySelector) for addItems, itemsList and items. The items is an array of objects (dishes) and each dish is going to have the input text (name of the dish) as well as the status of whether the dish has been checked or not.

Then we just add a listener on the form ‘submit’ event and then when someone submits an item we are going to add it to the list.

Next thing, we need to catch the user’s input and then add it to the array of dishes.

So, how do we capture the user’s input?

We can create a variable and query selector the value of it from the input field:

const text = (this.querySelector('[name=item]')).value;

This refers to our form, and we are interested in an element that has a name attribute of item.

It is a good practice to clear out the form once the user’s submitted it with this.reset().

Once we’ve added our dish to the items array, we want to display it.

In order to do that, we can make another function that will create our HTML list dynamically. It takes the items array and the destination HTML element as its arguments.

This function will simply loop over our array of items, and add each item as a

  • element along with its checkbox and label as an innerHTML of our destination element (itemsList).

    It is important to notice the join(”) function at the end. To set innerHTML we need the result of this function to be a string. Map function returns the array, so we convert it to string.

    join

    Another important thing to notice here is how the label and checkbox are linked together by the use of the same input id. Since id and label for are exactly the same thing.

    I really liked the custom checkbox that Wes implemented in a very nifty way with CSS:

    First by hiding the regular input checkbox:

    .dishes input {
        display: none;
    }    

    And then using the empty checkbox before the label if it is not checked:

    .dishes input + label:before {
        content: '⬜️';
        margin-right: 10px;
    }

    And when our html has the attribute of checked, then the input box changes to a taco 🌮🌮🌮:

    .dishes input:checked + label:before {
        content: '🌮';
    }
    
    <input type="checkbox" data-index=${i} id="item${i}" ${plate.done ? 'checked' : ''} />

    So far so good. However when we add a few items to our list and then mark a few of them as checked and refresh the page, our choices do not persist. That’s where the local storage comes in, where we are going to be storing our data.

    localStorage is a key:value store in the browser that has a list of things that have been saved. You are able to save this text to your browser, and when you reload the page, it will all still be there.

    In order to save our items to the local storage, we need to take the dishes and set it as a property of it. We cannot directly set our dishes to local storage as it does not accept any other input than string. We need to stringify our items array first and then store it.

    local storage

    Once we’ve saved our items to the local storage, we now have to make sure that after every reload the items still will be there. Instead of setting our const items to the empty array, we should retrieve the items from the storage first if there are any saved there:

    const items = JSON.parse(localStorage.getItem('items')) || [];

    We also need to persist the state of each item - whether is checked or not - on each page load. The inputs are created after we listen to them, so they don’t have the event listener. That’s why we cannot listen to click event on each item, instead we look for some html element that is going to be on the page at the time of loading and we listen to the click on that item. In our case, the parent unordered list is going to be available when our site loads. Using event delegation.

    The parent passes event onto the children.

    We create that event listener on our itemsList and then run the checked state toggle function.

    Now when we click on one of the items on the list, two events are fired on two items. Remember that the label and the checkbox are linked? Both of these elements are our event target if we don’t change it.

    In order to prevent that from happening, we want to verify that the target is matching the input:

        if (!e.target.matches('input')) return;

    And in order to toggle the state of this item, we need to know which item in the array we mean. For that, we created the data-index attribute. So when we click on item 2 on the list, we can toggle its done state accordingly:

    toggle code

    This post is becoming too long to hold content of the next five lessons, so it is time to get it shipped.

    And I will get back with part 3 for days 16-20 in the next few days!