JavaScript 30 – Day 5: Flex Panels Image Gallery

Welcome to my recap of Day 5 of the free JavaScript 30 online video course by Wes Bos. In case you really have not yet heard of the course, quickly head over to it and get started right away! 🙂

Please refer to the JavaScript 30 archive for all recap posts.

Objective

On Day 5, the task is to build an interactive image gallery heavily based on CSS3 flexbox, and JavaScript, of course.

The provided starter files include only a single HTML file with markup for the image gallery as well as some inline CSS that takes care of the basic, but incomplete styling. As usual, for my fork of the JavaScript 30 repository, I moved the CSS into a separate file.

As soon as the CSS was done, I stopped the video to take care of making the gallery work.

MisConception

Yeah, well, to be honest, I had some trouble with this, because there was no real description of how the image gallery should work. The video only included a short live demo—which I obviously didn’t pay enough attention to. 😀

To forestall, I didn’t get it right at first. And my final result still behaves different from what Wes implemented; I do like my gallery, though, that’s why I stuck to it. 🙂

But back to the conception. My initial one was something like this:

  • click on an image panel to make it open;
  • click on an open panel to make it revert to normal again;
  • when a panel gets opened, close all other (open) panels.

Implementation

Following the above conception, I came up with the following code:

const panels = Array.from( document.querySelectorAll( '.panel' ) );

panels.forEach( ( el ) => el.addEventListener( 'click', handleClick ) );

function handleClick() {
    if ( this.classList.contains( 'open' ) ) {
        closePanel( this );

        return;
    }

    openPanel( this );
}

function openPanel( panel ) {
    panels.filter( ( el ) => ( el !== panel ) ).forEach( closePanel );

    panel.classList.add( 'open' );
    panel.classList.add( 'open-active' );
}

function closePanel( panel ) {
    panel.classList.remove( 'open' );
    panel.classList.remove( 'open-active' );
}

Depending on whether or not a panel is open, clicking it makes it open or close. Opening a panel takes care of all other panels being closed. In order to easily filter the panels (see openPanel()), I created a real array from the node list that I initially got from querySelectorAll().

The gallery worked, and I continued with the video.

Reconception

I quickly realized that I did not yet reach what Wes had in mind. The two individual transitions, triggered by the open and open-active class, respectively, should not happen at the same time, but rather sequentially.

Okay, this calls for a transitionend event listener, like seen on Day 1. Let’s do this! 🙂

Reimplementation

Having revisited the conception, I adapted my code, and ended up with this:

const panels = Array.from( document.querySelectorAll( '.panel' ) );

panels.forEach( ( el ) => {
    el.addEventListener( 'click', handleClick );
    el.addEventListener( 'transitionend', handleTransition );
} );

function handleClick() {
    if ( this.classList.contains( 'open' ) ) {
        closePanel( this );

        return;
    }

    openPanel( this );
}

function openPanel( panel ) {
    panels.filter( ( el ) => ( el !== panel ) ).forEach( closePanel );

    panel.classList.add( 'open' );
}

function closePanel( panel ) {
    if ( panel.classList.contains( 'open' ) ) {
        panel.classList.add( 'closing' );
    }

    panel.classList.remove( 'open-active' );
}

function handleTransition( e ) {
    if ( this.classList.contains( 'closing' ) ) {
        if (
            ( this.classList.contains( 'open' ) && 'flex-grow' === e.propertyName )
            || 'transform' === e.propertyName
        ) {
            this.classList.remove( 'open' );
            this.classList.remove( 'closing' );
        }

        return;
    }

    if ( this.classList.contains( 'open' ) && 'flex-grow' === e.propertyName ) {
        this.classList.add( 'open-active' );
    }
}

Like mentioned before, I added another event listener that takes care of reacting to the appropriate transitions, and I also had to adapt my closePanel() function.

Since the opening phase of a panel was intended to be realized by using an openopen-active sequence, I assumed transitioning a panel to its normal state to be the other way around (i.e., first removing the open-active class, and then subsequently open). This is exactly what the above code is able to perform. I just had to introduce yet another class, closing, that is only used in JavaScript, and not CSS.

Refinement

Seeing Wes write up an implementation with only 10 LOC—and having a look again at my code—doesn’t feel that good, of course. 😀 One (major) difference, however, is that my gallery only allows for one open panel at a time. This means that opening a new panel implicitly reverts any other open one. And this again means that, by removing both the open-active and open class, further transitions are being kicked off. Maybe one of these days I will, again, revisit my code—and maybe I will even find a way to re-write it in a more concise form…

Another thing I learned from the video is that not all browsers use the same property name for flexbox-specific events. In order to take care of this, I improved the check for the flex property. The updated event listener callback now looks like so:

function handleTransition( e ) {
    if ( this.classList.contains( 'closing' ) ) {
        if (
            ( this.classList.contains( 'open' ) && e.propertyName.includes( 'flex' ) )
            || 'transform' === e.propertyName
        ) {
            this.classList.remove( 'open' );
            this.classList.remove( 'closing' );
        }

        return;
    }

    if ( this.classList.contains( 'open' ) && e.propertyName.includes( 'flex' ) ) {
        this.classList.add( 'open-active' );
    }
}

Live Demo

Want to see this in action? Well, here is a live demo. 🙂

Retrospection

On Day 5, I learned some new things about CSS flexbox. Besides playing (and localizing 🙂 ) Flexbox Froggy—which is real fun, by the way—I haven’t done too much with flexbox so far.

And yeah, having a complete and clear concept is of uttermost importance. I experienced this here on my own. Although, working as a web developer, this was by far not the first time I had trouble to deal with incomplete and/or inconsistent requirements. 😉

And You?

JavaScript 30 is free! So, unless you already did this on your own, what’s your reason not to? 🙂

2 Comments

  1. krischamp

    Hi Thorsten,
    Thanks very much for all your recaps … It helps a lot for a beginner JS dev to learn more with your concise style!!! I love that!!!
    I found a pb in your handleTransition function with :
    e.propertyName.contains( 'flex' ) >> should be : e.propertyName.includes( 'flex' )
    same correction further down …
    Thanks again for your nice vision of coding …

    1. Hi there,

      thanks a lot for the nice comment! And even more for making we aware of, and also fixing, a problem. 🙂

      Cheers,
      Thorsten

Leave a Reply

Your email address will not be published. Required fields are marked *